草庐IT

关于 Android启动优化你应该了解的知识点

Android技术圈 2023-09-21 原文

一、启动优化概念

1.1、为什么要做启动优化?

APP优化是我们进阶高级开发工程师的必经之路,而APP启动速度的优化,也是我们开启APP优化的第一步。用户在使用我们的软件时,交互最多最频繁的也就是APP的启动页面,如果启动页面加载过慢,很可能造成用户对我们APP的印象过差,进而消耗了用户的耐心,更严重可能导致用户的卸载行为。这也是微信始终坚持使用“一个小人望着地球”作为启动页面的背景,并且坚持不添加启动广告的的原因。

1.2、启动分类

冷启动: 特点是耗时最多,同时它也是衡量标准,我们在线上做的各种优化都是以它作为标准,从下面这张图片可以看出冷启动它经历了一系列的流程,所以它的耗时也是最多的。

热启动: 特点是最快,我们所说的热启动是指app从后台切换到前台,它没有application的创建和各种生命周期的调用,所以说这种启动方式是最快的。

温启动: 特点是较快,它的速度介于冷启动和热启动之间,对于这种方式它会重走activity的生命周期,不会重走进程的创建,application的创建和生命周期等流程。

1.3、相关任务

冷启动之前:

  1. 启动App;
  2. 加载空白Window;
  3. 创建进程。

这三个任务都是系统行为,无法进行真正的干预。网上大多介绍启动优化的都是针对第2条,但其实这是一个假的干预,只是对我们肉眼感知上的一个优化。

之后进行的是:

  1. 创建Application;
  2. 启动主线程;
  3. 创建MainActivity;
  4. 加载布局;
  5. 布置屏幕;
  6. 首帧绘制。

我们的优化方向: Application和Activity生命周期的这个阶段,这是开发者真正可以控制的时间。

二、启动时间测量方式

这里介绍两种启动时间的测量方式:

  1. adb命令
  2. 手动打点

2.1、adb命令

这种方式是我们通过在终端输入一条adb命令,然后它会打开我们要测试的app,同时进行结果的输出。具体的命令如下:

adb shell am start -W packagename/首屏Activity(这里需要使用全类名)

这里我以自己写的一个简单的列表展示的Demo工程举例说明:

ThisTime:最后一个Activity启动耗时

TotalTime:所有Activity启动耗时(这里ThisTime和TotalTime值是一致的,因为我的Demo中只有一个MainActivity)

WaitTime:AMS启动Activity的总耗时,对于一个通用的app(包含SplashActivity),ThisTime肯定是小于TotalTime的,即:

ThisTime < TotalTime < WaitTime

总结:这种方式线下使用方便,可以使用这种方式测量竞品为竞品分析提供需要的数据,不能带到线上,并且测量出来的时间也是一个非严谨精确的时间。

2.2、手动打点

这种方式是在app启动开始时埋点,启动结束时埋点,然后计算二者差值。

实际使用中,一般将开始时间这个点埋在Application的attachBaseContext(Context base)这个方法中,这是整个应用所能接收到的最早的回调时机。开始时间有了,那么结束时间该怎么计算呢,也就是我们应该把结束时间这个点埋在什么位置呢?网上很多资料里都会说是在onWindowFocusChanged()这个方法里做启动结束的时间计算,但是实际上写在这里其实是有问题的。

误区:onWindowFocusChanged它只是Activit的首帧时间,是activity首次绘制的时间,并不能代表activity已经展现出来。我们做性能优化的目的是为了改善用户的体验,并不是单纯的为了把启动时间缩短,因为这样做是不准确的,我们需要的是用户真正看到界面的时间,所以正确的情况应该是在真实的数据展示(一般取第一条)出来,才算结束的时间节点。

下面我们就来实战一下该如何在代码中埋点统计启动时间? 首先我们定义一个工具类LaunchTime,用来计算差值时间:

package com.jarchie.performance.utils;
 
import android.util.Log;
 
/**
 * 描述: 打点计算启动时间
 */
public class LaunchTime {
 
    private static long sTime;
 
    public static void startRecord() {
        sTime = System.currentTimeMillis();
    }
 
    public static void endRecord(String msg) {
        long cost = System.currentTimeMillis() - sTime;
        Log.i(msg, "--->cost" + cost);
    }
 
}

然后在Application中埋下开始时间点:

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    LaunchTime.startRecord();
}

然后在我们列表适配器中的onBindViewHolder中绑定数据时统计第一条Item展示出来的时间点:

if (position ==0 && !mHasRecorded){
            mHasRecorded = true;
            holder.mAllLayout.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    holder.mAllLayout.getViewTreeObserver().removeOnPreDrawListener(this);
                    LaunchTime.endRecord("FirstShow");
                    return true;
                }
            });
        }

最后我们在MainActivity中的onWindowFocusChanged()方法中统计一下Activity的首帧时间:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    LaunchTime.endRecord("onWindowFocusChanged");
}

现在来运行我们的程序,看一下最终统计出来的时间值究竟是多少?

由上面的结果可以看出首帧时间是904毫秒,列表数据的第一条展示的时间是1573毫秒,两者之间的时间差值是超过200毫秒的,这也就表明如果我们仅以Activity的首帧时间作为启动结束,那么这个时间明显是偏早的,不符合我们做启动优化的初衷。

三、启动优化工具

以下所介绍的两种方式是互相补充的,我们需要正确认识工具并且能够在不同的场景下选择适合的工具。

3.1、traceview

特点:

  • 图形的形式展示执行时间、调用栈等
  • 信息全面,包含所有线程

使用方式:在代码中需要做性能分析的地方开始位置和结束位置插入以下代码

  • Debug.startMethodTracing(""); //该方法具有重载方法,可设置收取信息的路径,大小
  • Debug.stopMethodTracing("");
  • 生成文件在sd卡:Android/data/packagename/files

代码实战:

将我们的项目运行之后(注意开启运行时权限,这部分不是本节重点,我直接到应用里将权限开启了)生成文件如下:

将生成的文件打开,如下图所示,可以看到Threads中是这个应用所有的线程数,我们可以看到线程总数以及对应的每个线程在具体的时间都做了哪些操作,然后下面有四个Tab可以切换,首先来看Call Chart,可以看到在具体的每一行都指向了具体的函数调用,将鼠标移到对应的每一行上面都有具体的执行时间等信息,沿着垂直方向看是具体的调用者,比如a调用b,则a在上方b在下方,而且不同的api它的颜色也是不一样的,对于系统api是橙色的,对于应用自身的函数调用颜色是绿色的,对于第三方api调用颜色是蓝色的(包括java语言的api)。

接着来看Flame Chart(又叫火焰图),它是一个倒置的调用图表,一般来说它的作用没有第一个大,它会收集相同的调用方顺序完全相同的函数,比如a调b调c并且调用了多次,它会将它们收集在一起:

下面这张图是Top Down,它比较直观的展示了函数的调用列表,比如下图中首先main()函数调用了init(),init()又调用了g()等等,相当于Call Chart详细版,并且你将鼠标放在对应函数上右键有个Jump to Source可以跳转到具体的代码中。

Total Time是某个函数执行的总时间,Self Time是该函数体内部自有代码执行的时间,Childre Time是该函数内部调用别的函数所需的时间,后面二者的时间总和一定是等于前面的Total Time的,这点需要注意。Self Time上方有一栏下拉菜单,即我们第一张图中红色标注的菜单栏WallClockTime和ThreadTime,前者是这段代码执行所消耗的时间,后者是CPU执行的时间,一般情况下是前者大于后者,因为一般情况下某个函数消耗的时间并不等于CPU真正消耗的时间。

最后是Bottom Up,这个的作用也是比较小了,它和Top Down是相反的,它会告诉你某个函数具体是谁调用了它:

总结:一般比较关注的是Call Chart和Top Down

  • 运行时开销严重,整体都会变慢(它会抓取当前运行的所有线程的所有执行函数和顺序)
  • 由于它非常严重的运行时开销,所以它很有可能回带偏优化方向

3.2、systrace(python脚本)

特点:

  • 结合Android内核的数据,生成Html报告
  • API18以上使用,推荐TraceCompat

使用方式:

我这里放一张我自己运行的示例,仅供参考:

代码实战:

首先将项目运行让我们写的代码生效,然后运行我们的python脚本,启动tracing之后,点击我们的app让它开始收集信息,tracing完成之后到对应的目录就会发现已经生成了我们的Performance.html文件,我们到浏览器中打开,如下图所示:

由上图中左侧可以看到有CPU的核心数,往下滑动还可以看到各个线程名称,然后还可以根据代码中打的Tag来搜索,下方会展示比较详细的trace信息(上图中也举例说明了,需要注意的Wall Time和CPU Time红色部分圈出),点到右侧图中具体的位置都会展示出比较详细的方法名称执行时间等信息。

总结:

  • 轻量级,开销小(它是你在哪里埋点,它就处理哪里,这点和traceview不同,需要注意)
  • 直观反映cpu利用率
  • 需要注意cputime与walltime区别: walltime是代码执行时间,cputime是代码消耗cpu的时间(优化的重点指标)。举个栗子:锁冲突(比如现在调用了A方法,进入A方法之后需要一把锁,但此时这把锁被B所持有,导致代码在A这里停下了,实际上可能这个A函数并不耗时,但是由于一直拿不到锁,所以一直处于等待状态,这就导致walltime时间很长,但是它实际上对CPU并没有多少消耗)

关于上述工具的详细使用方法大家可以自行百度或者谷歌查找相关资料,认真学习一下这些分析工具的使用。

四、优雅获取方法耗时

4.1、常规方式

我们在做启动优化的时候通常需要知道启动阶段所有方法的耗时,这样可以有针对性的分析出耗时较多的方法。一般的实现方式就是通过手动埋点来实现,比如在某个方法开始和结束的位置分别插入以下代码:

long time = System.currentTimeMillis();
initJpush();
long cost = System.currentTimeMillis() - time;
//或者可以使用这行代码:SystemClock.currentThreadTimeMillis(); 
//CPU真正执行的时间

当有多个方法需要埋点时,同理这样写就可以获取到每个方法的执行时间了,但是这样操作存在的问题也是显而易见的,当然我相信你肯定也发现了,主要总结为以下几点:

  • 代码重复、耦合度高并且看起来非常恶心
  • 侵入性强
  • 工作量大

那么针对这种方式的劣势,如何才能更加优雅的实现获取方法的耗时呢?答案就是采用AOP的方式来实现。

4.2、AOP介绍

AOP简介:Aspect Oriented Programming,面向切面编程

  • 针对同一类问题的统一处理
  • 无侵入添加代码

AspectJ简介:它就是辅助AOP用来实现切面编程

使用时首先需要添加如下的依赖:

//工程目录下的build.gradle
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
//app module目录下的build.gradle
apply plugin: 'android-aspectjx'
implementation 'org.aspectj:aspectjrt:1.8.14'

添加完了依赖之后,再来介绍一下相关知识点,然后我们再到代码中去真正的使用它。

Join Points:程序运行时的执行点,常用的可以作为切面的地方如下所示:

  • 函数调用、执行
  • 获取、设置变量
  • 类初始化

PointCut:带条件的JoinPoints

Advice:一种Hook,要插入代码的位置

  • Before:PointCut之前执行

  • After:PointCut之后执行

  • Around:PointCut之前、之后分别执行

  • 举个栗子:

    @Before("execution(* android.app.Activity.on**(..))")
    public void onActivityCalled(JoinPoint joinPoint) throws Throwable{ ... }
    
    

语法简介:

  • Before:Advice,具体插入的位置
  • execution:处理Join Point的类型,call、execution
  • (* android.app.Activity.on**(..)):匹配规则,匹配android.app.Activity类中任意返回值类型的on开头的是否有参数的方法都行
  • onActivityCalled:要插入的代码

代码实现:

/**
 * 说明:使用AOP方式来统计方法耗时
 */
@Aspect //通过该注解,AOP框架可以知道该类即是需要需要插入的代码
public class PerformanceAop {
 
    @Around("call(* com.jarchie.performance.app.BaseApp.**(..))") //匹配规则
    public void calculateTime(ProceedingJoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature(); //拿到切点签名
        String name = signature.toShortString(); //拿到对应的方法信息
        long time = System.currentTimeMillis();
        try {
            joinPoint.proceed(); //手动执行
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.i("执行时间", name + "--->>>" + (System.currentTimeMillis() - time));
    }
 
}

可以看到这里是新建了一个类采用AOP的方式来获取方法耗时,并没有在BaseApp中添加任何的代码,运行结果如下所示:

总结:

采用AOP实现:

  1. 无侵入性
  2. 修改方便

五、异步优化

5.1、Theme切换

首先需要说明的是这种方式仅仅是给用户感官上的快,just a feeling,对应用真实的启动速度没有任何的影响。它的实现原理是App在打开首屏Activity之前会首先显示出一张图片,当Activity页面真正展示出来之后再把Theme改变回来,因为冷启动中有一步是创建一个空白的Window,这种实现方式正式利用了这个空白的Window。下面来看下具体怎么操作:

首先定义一个背景drawable,这里起名为launcher.drawable:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">
    <item android:drawable="@color/colorPrimary" />
    <item>
        <bitmap
            android:gravity="center"
            android:src="@drawable/liying" />
    </item>
</layer-list>

然后在styles中定义一个主题作为启动主题:

    <style name="Theme.Splash" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowBackground">@drawable/launcher</item>
        <item name="windowActionBar">false</item>
        <item name="android:windowNoTitle">true</item>
        <item name="windowNoTitle">true</item>
        <item name="android:windowFullscreen">true</item>
    </style>

然后在首屏Activity的清单文件中设置这个主题:

        <activity android:name=".MainActivity"
            android:theme="@style/Theme.Splash">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

最后在首屏Activity的onCreate()方法中调用父类onCreate()方法之前将设置的启动主题改为默认主题:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
    }

来看下我们修改后的效果,如下图所示:

5.2、常规异步优化

核心思想:子线程分担主线程任务,并行减少时间

下面还以Application的onCreate()为例分析常规的异步优化:现在的App一般情况下都是运行在八核的设备上,不同的设备厂商可能分配给应用的核数有的四核有的八核,但是如果像我们这里的代码将所有的初始化工作都放在一个线程中最多占用一个核,别的三个核或者七个核都处于一个浪费状态,那么为了让CPU的利用率达到一个更加高效的状态,这里就需要使用异步初始化了。

说到异步,那大家想到的肯定是要创建子线程了,这里使用线程池来创建线程,这种方式更加优雅,不仅可以在很大程度上避免内存泄露,而且还可以让线程得到复用(这里线程数的设置是参考了Android AsyncTask源码中的设计):

private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = Math.max(2,Math.min(CPU_COUNT-1,4));
 
@SuppressLint("MissingPermission")
@Override
public void onCreate() {
    super.onCreate();
    LaunchTime.startRecord();
    mApplication = this;
    ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
    service.submit(this::initDeviceId);
    service.submit(this::initJpush);
    service.submit(this::initBugly);
    LaunchTime.endRecord("AppOnCreate");
}

来看下运行结果:

可以看到时间确实是非常短的,那现在有个问题:是不是以后代码都可以放在子线程中执行呢?答案当然是否定的,有些场景下并不能很好的实现异步的方案,比如:①有些代码必须要在主线程中执行;②有些方法必须在onCreate()方法结束后执行完毕。

针对上面这两种情况,异步的方案其实就不太好解决了,对于第一种情况你只能放弃异步方案,对于第二种情况,我们可以采用CountDownLatch这个类来解决,下面这段代码的含义大致就是:只要countDownLatch不被满足,它将一直处于等待状态,直到被满足1次,因为我们构造函数中传入的数值是1:

private CountDownLatch mCountDownLatch = new CountDownLatch(1);
ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
        service.submit(() -> {
            initBugly();
            mCountDownLatch.countDown();
        });
        try {
            mCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

异步优化注意事项:

  • 不符合异步要求(如果能修改成符合要求的则修改,不能修改则放弃异步方案)
  • 需要在某阶段完成
  • 区分CPU密集型和IO密集型任务

5.3、启动器

通过上面的常规异步操作过程可以发现还是存在很多问题的,主要有以下几点:

  • 代码不优雅(假如方法数较多时,则会写很多重复代码)
  • 场景不好处理(特定阶段执行完毕、依赖关系)
  • 维护成本高

正是因为有上面这些问题的存在,才有了下面的解决方案的产生——启动器。

推荐阿里开源的一个启动器库alpha:github.com/alibaba/alp…

启动器介绍:

核心思想:充分利用CPU多核,自动梳理任务顺序

启动器流程:

  • 代码Task化,启动逻辑抽象为Task
  • 根据所有任务依赖关系排序生成一个有向无环图(自动生成的)
  • 多线程按照排序后的优先级依次执行

启动器流程图:

代码实战:首先构建启动器部分的代码因为这个过程还是有点复杂的,代码相对也不少,这里就不贴了,大家可以自行百度启动器相关的实现代码,这里只针对使用情况做一个说明:

首先我们需要将上面做异步操作的几个方法抽成对应的任务,比如这里InitBuglyTask这个任务就是对应用来解决需要在特定阶段完成初始化的问题,重写needWait()方法设置为true即需要等待,并且MainTask是运行在主线程的:

public class InitBuglyTask extends MainTask {
 
    //解决特定阶段执行完成问题
    @Override
    public boolean needWait() {
        return true;
    }
 
    @Override
    public void run() {
        CrashReport.initCrashReport(mContext, "e296ad7fc8", false);
    }
}

然后定义InitDeviceIdTask这个用来获取设备ID的任务,该任务是在子线程执行的:

public class InitDeviceIdTask extends Task {
    private String mDeviceId;
 
    @SuppressLint("MissingPermission")
    @Override
    public void run() {
        //真正自己的代码
        TelephonyManager tManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
        mDeviceId = tManager.getDeviceId();
    }
}

然后定义初始化极光推送的任务InitJpushTask,重写dependsOn()方法用来解决依赖关系的问题,该任务的执行依赖于设备ID:

public class InitJpushTask extends Task {
 
    //解决依赖关系问题
    @Override
    public List<Class<? extends Task>> dependsOn() {
        List<Class<? extends Task>> task = new ArrayList<>();
        task.add(InitDeviceIdTask.class);
        return task;
    }
 
    @Override
    public void run() {
        //推送
        JPushInterface.init(mContext);
    }
}

然后将这些任务添加到启动器里面即可,代码看起来还是比较美观的:

LaunchTime.startRecord();
TaskDispatcher.init(this);
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
dispatcher.addTask(new InitBuglyTask())
    .addTask(new InitJpushTask())
    .addTask(new InitDeviceIdTask())
    .start();
dispatcher.await();
LaunchTime.endRecord("AppOnCreate");

OK,通过上面这几行代码启动器就搞定了,可见它相比较于传统的异步方式还是好处多多啊,最后来看下运行结果吧:

六、延迟初始化

6.1、常规方案

对于实际项目经验较多的朋友你会发现其实在Application或者MainActivity中有些任务它的优先级并不是很高,所以对于这类任务通常都可以将它们进行延迟初始化,一般都是延迟到列表数据展示之后再进行加载。我们首先来看下常规的方案是如何实现的呢?最简单的做法就是将代码移到列表显示之后进行调用,或者是通过new Handler().postDelayed延迟一个时间调用。即:

  • New Handler().postDelayed
  • Feed展示后调用

下面我们在代码中举个栗子说明一下这种方案是如何实现的?

这里首先定义一个回调接口是在列表展示出来之后的回调:

public interface OnFeedShowCallBack {
    void onFeedShow();
}

然后在列表适配器中定义这个接口,并给它一个setXXX()方法,并且在列表item第一条展示出来之后回调这个接口:

private OnFeedShowCallBack mCallBack;
...
public void setOnFeedShowCallBack(OnFeedShowCallBack callBack){
    this.mCallBack = callBack;
}
...
if (mCallBack!=null){
    mCallBack.onFeedShow();
}

接着在MainActivity的onCreate()中设置这个回调,并且让MainActivity实现回调接口重写回调方法,在回调方法中模拟执行两个Task,整个这个流程如果熟悉接口回调机制的兄弟应该很好理解了:

mAdapter.setOnFeedShowCallBack(this);
...
@Override
public void onFeedShow() {
    //模拟执行了两个Task,TaskA和TaskB
    new DispatchRunnable(new DelayInitTaskA()).run();
    new DispatchRunnable(new DelayInitTaskB()).run();
}

以上就是常规方案的实现方法,大家仔细思考一下会发现这其中是有很多问题的:首先,我们的列表展示是发生在主线程中,直接执行mCallBack.onFeedShow()方法,会跑到MainActivity重写的onFeedShow()中,如果模拟的任务执行时间较长,那么主线程就会相应的卡住对应的时长,如果此时用户滑动列表很明显会造成列表滑动卡顿,给用户的体验就很不好了。如果你采用new Handler().postDelayed发送延时消息来处理,当然一定程度上是可以缓解这种卡段,但是这种方案总结下来延时的时机不太好控制并且如果任务数量较多也不易维护,所以我们需要去寻求更加优雅的解决方案。

6.2、优雅实现延迟初始化

核心思想: 对延迟任务进行分批初始化,这里利用IdleHandler特性,空闲执行

针对这种方案我们在代码中来实践一下看看具体该如何操作?

首先来创建一个针对延迟初始化任务执行的启动器:

public class DelayInitDispatcher {
 
    //创建任务队列
    private Queue<Task> mDelayTasks = new LinkedList<>();
 
    //IdleHandler分批处理并在系统空闲时执行
    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() { //系统空闲时回调
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll(); //分批执行,每次只取一个Task
                new DispatchRunnable(task).run();
            }
            return !mDelayTasks.isEmpty(); //DelayTasks为空则移除
        }
    };
 
    //添加任务
    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }
 
    //启动
    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }
 
}

具体的代码含义都加了注释了,主要就是利用了IdleHandler的特性在空闲时期执行,接着在onFeedShow()的回调中添加任务并执行即可:

@Override
public void onFeedShow() {
    DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
    delayInitDispatcher.addTask(new DelayInitTaskA())
        .addTask(new DelayInitTaskB())
        .start();
}

通过代码我们来比对一下两种方案的差别:对于常规方案回调接口中有多少个任务都会一次性执行完成,也就意味着主线程会卡在那里对应的时间;对于第二种方案,我们是添加了多个任务进来,执行的时机是在系统空闲的时候进行执行,并且一次只执行一个,所以第二种方案的优点就显而易见了:

  • 执行时机明确;
  • 有效缓解列表卡顿,它可以真正的提升用户的体验。

七、启动优化其他方案

7.1、优化总方针

  • 异步、延迟、懒加载(与实际业务强相关,哪里使用哪里加载)
  • 技术、业务相结合

注意事项:

  1. wall time和cpu time的区别
  • cpu time才是优化方向
  • 按照systrace及cpu time跑满cpu
  1. 监控的完善
  • 线上监控多阶段时间(App、Activity、声明周期间隔时间)
  • 将监控信息上报后台,处理聚合看趋势
  1. 收敛启动代码修改权限
  • 结合CI修改启动代码需要Review或通知

7.2、启动优化其他方案

这一部分只是简单介绍一下其他的启动优化的方案,有些方案实现起来还是比较复杂的,有需要的朋友可以查找相关资料结合自身项目实践一下。

1. 提前加载SharedPreferences:使用之前会调用getSharedPreference()方法,此时会去异步加载文件中它的配置文件xml并将它load到内存之中,当我们put或者get某个属性时如果load没有完成则会阻塞一直等待

  • Multidex之前加载,利用此阶段CPU
  • 覆写getApplicationContext返回this

2. 启动阶段不启动子进程

  • 子进程会共享CPU资源、导致主进程CPU资源紧张
  • 注意启动顺序:App onCreate之前是ContentProvider(启动阶段不要启动其他组件)

3. 类加载优化:提前异步类加载

  • Class.forName()只加载类本身及其静态变量的引用类(需要发生在异步线程中)
  • new类实例可以额外加载类成员变量的引用类

4. 启动阶段抑制GC(Native Hook)

OK,写到这里相信你已经对Android启动优化有了自己的了解了,可能我这里介绍的不够全面,因为个人能力有限,所以对于哪些说的不够清楚的地方大家就再查找相关的资料进行更加细致的学习吧。

有关关于 Android启动优化你应该了解的知识点的更多相关文章

  1. ruby - 检查 "command"的输出应该包含 NilClass 的意外崩溃 - 2

    为了将Cucumber用于命令行脚本,我按照提供的说明安装了arubagem。它在我的Gemfile中,我可以验证是否安装了正确的版本并且我已经包含了require'aruba/cucumber'在'features/env.rb'中为了确保它能正常工作,我写了以下场景:@announceScenario:Testingcucumber/arubaGivenablankslateThentheoutputfrom"ls-la"shouldcontain"drw"假设事情应该失败。它确实失败了,但失败的原因是错误的:@announceScenario:Testingcucumber/ar

  2. ruby-on-rails - 启动 Rails 服务器时 ImageMagick 的警告 - 2

    最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru

  3. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  4. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  5. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  6. ruby-on-rails - 带有 Zeus 的 RSpec 3.1,我应该在 spec_helper 中要求 'rspec/rails' 吗? - 2

    使用rspec-rails3.0+,测试设置分为spec_helper和rails_helper我注意到生成的spec_helper不需要'rspec/rails'。这会导致zeus崩溃:spec_helper.rb:5:in`':undefinedmethod`configure'forRSpec:Module(NoMethodError)对thisissue最常见的回应是需要'rspec/rails'。但这是否会破坏仅使用spec_helper拆分rails规范和PORO规范的全部目的?或者这无关紧要,因为Zeus无论如何都会预加载Rails?我应该在我的spec_helper中做

  7. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  8. ruby - 了解在 Ruby 中与 lambda 一起使用的 inject 行为 - 2

    我经常将预配置的lambda插入可枚举的方法中,例如“map”、“select”等。但是“注入(inject)”的行为似乎有所不同。例如与mult4=lambda{|item|item*4}然后(5..10).map&mult4给我[20,24,28,32,36,40]但是,如果我制作一个2参数lambda用于像这样的注入(inject),multL=lambda{|product,n|product*n}我想说(5..10).inject(2)&multL因为“inject”有一个可选的单个初始值参数,但这给了我......irb(main):027:0>(5..10).inject

  9. ruby-on-rails - 关于 Ruby 的一般问题 - 2

    我在我的rails应用程序中安装了来自github.com的acts_as_versioned插件,但有一段代码我不完全理解,我希望有人能帮我解决这个问题class_eval我知道block内的方法(或任何它是什么)被定义为类内的实例方法,但我在插件的任何地方都找不到定义为常量的CLASS_METHODS,而且我也不确定是什么here,并且有问题的代码从lib/acts_as_versioned.rb的第199行开始。如果有人愿意告诉我这里的内幕,我将不胜感激。谢谢-C 最佳答案 这是一个异端。http://en.wikipedia

  10. ruby-on-rails - 如何测试自己对 Ruby/ROR 的了解? - 2

    是否有self验证的问题列表。看着那个,我可以确定我知道。我应该复习一下。在学习的过程中,我列了一个这样的list,但它只包含我在某处听说过的项目。我需要一段时间才能找到新的东西。 最佳答案 以下是针对ruby​​和Rails的一些测试列表。证书名称:RubyonRails谁提供:oDeskIncorporation认证费用:免费网站:https://www.odesk.com/tests/985?pos=0证书名称:RubyonRails提供者:Techgig.com(TimesBusinessSolutionsLimited(T

随机推荐