Android启动优化 :学会这些让应用启动速度提高10倍!

作者:胡飞洋
链接:https://juejin.im/post/5f183026f265da230739b7db

一、概述

手机桌面点击一个应用,用户希望应用能及时响应、快速加载。启动时间过长的应用可能会令用户失望。这种糟糕的体验可能会导致用户在 Play 商店针对您的应用给出很低的评分,甚至完全弃用您的应用。

本篇就来讲解如何分析和优化应用的启动时间。首先介绍启动过程机制,然后讨论如何检测启动时间以及分析工具,最后给出通用启动优化方案。

二、应用启动流程介绍

根据官方文档,应用有三种启动状态:冷启动温启动热启动

  • 冷启动 冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。例如,通过任务列表手动杀掉应用进程后,又重新启动应用。

  • 热启动 热启动比冷启动简单得多,开销也更低。在热启动中,系统的所有工作就是将您的 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行进程、应用、activity的创建。例如,按home键到桌面,然后又点图标启动应用。

  • 温启动 温启动包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:用户按返回键退出应用后又重新启动应用。这时进程已在运行,但应用必须通过调用 onCreate() 从头开始重新创建 Activity。

启动优化是在 冷启动 的基础上进行优化。要优化应用以实现快速启动,了解系统和应用层面的情况以及它们在各个状态中的互动方式很有帮助。

在冷启动开始时,系统有三个任务,它们是:

  • 加载并启动应用。
  • 在启动后立即显示应用的空白启动窗口。
  • 创建应用进程。

系统一创建应用进程,应用进程就负责后续阶段

  • 启动主线程。
  • 创建应用对象。
  • 创建主 Activity。
  • 加载视图。
  • 执行初始绘制。

一旦应用进程完成第一次绘制,系统进程就会换掉当前显示的后台窗口(StartingWindow),替换为主 Activity。此时,用户可以开始使用应用。

下面是[官方文档]中的启动过程流程图,显示系统进程和应用进程之间如何交接工作。实际上对启动流程的简要概括。

三、优化核心思想

问题来了,启动优化是对 启动流程的那些步骤进行优化呢?

这是一个好问题。我们知道,用户关心的是:点击桌面图标后 要尽快的显示第一个页面,并且能够进行交互。 根据启动流程的分析,显示页面能和用户交互,这是主线程做的事情。那么就要求 我们不能再主线程做耗时的操作。启动中的系统任务我们无法干预,能干预的就是在创建应用和创建 Activity 的过程中可能会出现的性能问题。这一过程具体就是:

  • Application的attachBaseContext
  • Application的onCreate
  • activity的onCreate
  • activity的onStart
  • activity的onResume

activity的onResume方法完成后才开始首帧的绘制。所以这些方法中的耗时操作我们是要极力避免的。

并且,通常情况下,一个应用的主页的数据是需要进行网络请求的,那么用户启动应用是希望快速进入主页以及看到主页数据,这也是我们计算启动结束时间的一个依据。

四、时间检测

4.1 Displayed

在 Android 4.4(API 级别 19)及更高版本中,logcat 包含一个输出行,其中包含名为 “Displayed” 的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。经过的时间包括以下事件序列:

  • 启动进程。
  • 初始化对象。
  • 创建并初始化 Activity。
  • 扩充布局。
  • 首次绘制。

这是我的demo app 启动的日志打印,查看

2020-07-13 19:54:38.256 18137-18137/com.hfy.androidlearning I/hfy: onResume begin. 
2020-07-13 19:54:38.257 18137-18137/com.hfy.androidlearning I/hfy: onResume end. 
2020-07-13 19:54:38.269 1797-16782/? I/WindowManager: addWindow: Window{1402051 u0 com.hfy.androidlearning/com.hfy.demo01.MainActivity}
2020-07-13 19:54:38.391 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s251ms
复制代码

可见“Displayed”的时间打印是在添加window之后,而添加window是在onResume方法之后。

4.2 adb shell

也可以使用adb命令运行应用来测量初步显示所用时间:

adb shell am start -W [ApplicationId]/[根Activity的全路径] 当ApplicationId和package相同时,根Activity全路径可以省略前面的packageName。

Displayed 指标和前面一样出现在 logcat 输出中:

2020-07-14 14:53:05.294 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s98ms
复制代码

您的终端窗口在adb命令执行后还应显示以下内容:

hufeiyangdeMacBook-Pro:~ hufeiyang$ adb shell am start -W com.hfy.androidlearning/com.hfy.demo01.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.hfy.androidlearning/com.hfy.demo01.MainActivity }
Status: ok
LaunchState: COLD
Activity: com.hfy.androidlearning/com.hfy.demo01.MainActivity
TotalTime: 2098
WaitTime: 2100
Complete
复制代码

我们关注TotalTime即可,即应用的启动时间,包括 创建进程 + Application初始化 + Activity初始化到界面显示 的过程。

4.3 reportFullyDrawn()

可以使用 reportFullyDrawn() (API19及以上)方法测量从应用启动到完全显示所有资源和视图层次结构所用的时间。什么意思呢?前面核心思想中提到,主页数据请求后完全呈现界面的过程也是一个优化点,而前面的“Displayed”、:“TotalTime”的时间统计都是启动到首帧绘制,那么如何获取 从 启动 到 获取网络请求后再次完成刷新 的时间呢?

要解决此问题,您可以手动调用Activity的 reportFullyDrawn()方法,让系统知道您的 Activity 已完成延迟加载。当您使用此方法时,logcat 显示的值为从创建应用对象到调用 reportFullyDrawn() 时所用的时间。使用示例如下:

    @Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                            reportFullyDrawn();
                        }
                    }
                });

            }
        }).start();
    }
复制代码

使用子线程睡1秒来模拟数据加载,然后调用reportFullyDrawn(),以下是 logcat 的输出。

2020-07-14 15:26:00.979 1797-2017/? I/ActivityTaskManager: Displayed com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s133ms
2020-07-14 15:26:01.788 1797-2017/? I/ActivityTaskManager: Fully drawn com.hfy.androidlearning/com.hfy.demo01.MainActivity: +2s943ms
复制代码

4.4 代码打点

写一个打点工具类,开始结束时分别记录,把时间上报到服务器。

此方法可带到线上,但代码有侵入性。

开始记录的位置放在 Application 的 attachBaseContext 方法中,attachBaseContext 是我们应用能接收到的最早的一个生命周期回调方法。

计算启动结束时间的两种方式

  • 一种是在 onWindowFocusChanged 方法中计算启动耗时。 onWindowFocusChanged 方法只是 Activity 的首帧时间,是 Activity 首次进行绘制的时间,首帧时间和界面完整展示出来还有一段时间差,不能真正代表界面已经展现出来了。

  • 按首帧时间计算启动耗时并不准确,我们要的是用户真正看到我们界面的时间。 正确的计算启动耗时的时机是要等真实的数据展示出来,比如在列表第一项的展示时再计算启动耗时。 (在 Adapter 中记录启动耗时要加一个布尔值变量进行判断,避免 onBindViewHolder 方法被多次调用导致不必要的计算。)

//第一个item 且没有记录过,就结束打点
  if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
      mHasRecorded = true;
      helper.getView(R.id.xxx).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
          @Override
          public boolean onPreDraw() {
              helper.getView(R.id.xxx).getViewTreeObserver().removeOnPreDrawListener(this);
              LogHelper.i("结束打点!");
              return true;
          }
      });
  }

复制代码

4.5 AOP(Aspect Oriented Programming) 打点

面向切面编程,可以使用AspectJ。例如可以切Application的onCreate方法来计算其耗时。 特点是是对代码无侵入性、可带到线上。

五、分析工具介绍

分析方法耗时的工具: Systrace 、 Traceview,两个是相互补充的关系,我们要在不同的场景下使用不同的工具,这样才能发挥工具的最大作用。

5.1 Traceview

Traceview 能以图形的形式展示代码的执行时间和调用栈信息,而且 Traceview 提供的信息非常全面,因为它包含了所有线程。

Traceview 的使用可以分为两步:开始跟踪、分析结果。我们来看看具体操作。

通过 Debug.startMethodTracing(tracepath) 开始跟踪方法,记录一段时间内的 CPU 使用情况。调用 Debug.stopMethodTracing() 停止跟踪方法,然后系统就会为我们生成一个.trace文件,我们可以通过 Traceview 查看这个文件记录的内容。

文件生成的位置默认在 Android/data/包名/files 下,下面来看一个例子。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "onCreate begin. ");
        super.onCreate(savedInstanceState);
        //默认生成路径:Android/data/包名/files/dmtrace.trace
        Debug.startMethodTracing();
        //也可以自定义路径
       //Debug.startMethodTracing(getExternalFilesDir(null)+"test.trace");

        setContentView(R.layout.activity_main);
        Intent intent = getIntent();
        String name = intent.getStringExtra("name");
        Log.i(TAG, "onCreate: name = " + name);

        initConfig();
        initView();
        initData();
        ...

        Debug.stopMethodTracing();
    }
复制代码

在MainActivity的onCreate前后方法中分别调用开始停止记录方法,运行打开应用进入首页后,我们定位到 /sdcard/android/data/包名/files/ 目录下查看文件管理器确实是有.trace文件:

然后双击打开:


以图形来呈现方法跟踪数据或函数跟踪数据,其中调用的时间段和时间在横轴上表示,而其被调用方则在纵轴上显示。 所以我们可以看到具体的方法及其耗时。

详细介绍参考官方文档《使用 CPU Profiler 检查 CPU 活动》。

可以看到在onCreate方法中,最耗时的是testHandler方法,它里面睡了一觉。

5.2 Systrace

Systrace 结合了 Android 内核数据,分析了线程活动后会给我们生成一个非常精确 HTML 格式的报告。

Systrace原理:在系统的一些关键链路(如SystemServcie、虚拟机、Binder驱动)插入一些信息(Label)。然后,通过Label的开始和结束来确定某个核心过程的执行时间,并把这些Label信息收集起来得到系统关键路径的运行时间信息,最后得到整个系统的运行性能信息。其中,Android Framework 里面一些重要的模块都插入了label信息,用户App中也可以添加自定义的Lable。

Systrace 提供的 Trace 工具类默认只能 API 18 以上的项目中才能使用,如果我们的兼容版本低于 API 18,我们可以使用 TraceCompat。 Systrace 的使用步骤和 Traceview 差不多,分为下面两步。

  • 调用跟踪方法
  • 查看跟踪结果

来看示例,在onCreate前后分别使用TraceCompat.beginSection、TraceCompat.endSection方法:

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        Log.i(TAG, "onCreate begin. ");

        super.onCreate(savedInstanceState);

        TraceCompat.beginSection("MainActivity onCreate");

        Debug.startMethodTracing();//dmtrace.trace
//        Debug.startMethodTracing(getExternalFilesDir(null)+"test.trace");

        setContentView(R.layout.activity_main);

        initConfig();
        initView();
        initData();

        Debug.stopMethodTracing();

        TraceCompat.endSection();
    }
复制代码

运行app后,手动杀掉。然后cd 到SDK 目录下的 platform-tools/systrace 下,使用命令:

python systrace.py -t 10 -o /Users/hufeiyang/trace.html -a com.hfy.androidlearning

其中:-t 10是指跟踪10秒,-o 表示把文件输出到指定目录下,-a 是指定应用包名。

输入完这行命令后,可以看到开始跟踪的提示。看到 “Starting tracing ”后,手动打开我们的应用。

示例如下:

hufeiyangdeMacBook-Pro:~ hufeiyang$ cd  /Users/hufeiyang/Library/Android/sdk/platform-tools/systrace

hufeiyangdeMacBook-Pro:systrace hufeiyang$ python systrace.py -t 10 -o /Users/hufeiyang/trace.html  -a com.hfy.androidlearning

Starting tracing (10 seconds)
Tracing completed. Collecting output...
Outputting Systrace results...
Tracing complete, writing results

Wrote trace HTML file: file:///Users/hufeiyang/trace.html
复制代码

跟踪10秒,然后就在指定目录生成了html文件,我们打开看看:

这里我们同样可以看到具体的耗时,以及每一帧渲染耗费的时间。具体参考官方文档《Systrace 概览》

小结 Traceview 的两个特点

  • 可埋点 Traceview 的好处之一是可以在代码中埋点,埋点后可以用 CPU Profiler 进行分析。 因为我们现在优化的是启动阶段的代码,如果我们打开 App 后直接通过 CPU Profiler 进行记录的话,就要求你有单身三十年的手速,点击开始记录的时间要和应用的启动时间完全一致。 有了 Traceview,哪怕你是老年人手速也可以记录启动过程涉及的调用栈信息。
  • 开销大 Traceview 的运行时开销非常大,它会导致我们程序的运行变慢。 之所以会变慢,是因为它会通过虚拟机的 Profiler 抓取我们当前所有线程的所有调用堆栈。 因为这个问题,Traceview 也可能会带偏我们的优化方向。 比如我们有一个方法,这个方法在正常情况下的耗时不大,但是加上了 Traceview 之后可能会发现它的耗时变成了原来的十倍甚至更多。

Systrace 的两个特点

  • 开销小 Systrace 开销非常小,不像 Traceview,因为它只会在我们埋点区间进行记录。 而 Traceview 是会把所有的线程的堆栈调用情况都记录下来。
  • 直观 在 Systrace 中我们可以很直观地看到 CPU 利用率的情况。 当我们发现 CPU 利用率低的时候,我们可以考虑让更多代码以异步的方式执行,以提高 CPU 利用率。

Traceview 与 Systrace 的两个区别

  • 查看工具 Traceview 分析结果要使用 Profiler 查看。 Systrace 分析结果是在浏览器查看 HTML 文件。
  • 埋点工具类 Traceview 使用的是 Debug.startMethodTracing()。 Systrace 用的是 Trace.beginSection() 和 TraceCompat.beginSection()。

六、启动优化方案

优化方案有两个方向:

  • 视觉优化,启动耗时没有变少,但是启动过程中给用户更好的体验。
  • 速度优化,减少主线程的耗时,真实做到快速启动。

6.1 视觉优化

《Activity的启动》中提到,在Activity启动前会展示一个名字叫StartingWindow的window,这个window的背景是取要启动Activity的Theme中配置的WindowBackground。

因为启动根activity前是需要创建进程等一系列操作,需要一定时间,而展示StartingWindow的目的是 告诉用户你点击是有反应的,只是在处理中,然后Activity启动后,Activity的window就替换掉这个StartingWindow了。如果没有这个StartingWindow,那么点击后就会一段时间没有反应,给用户误解。

而这,就是应用启动开始时 会展示白屏的原因了。

那么视觉优化的方案 也就有了:替换第一个activity(通常是闪屏页)的Theme,把白色背景换成Logot图,然后再Activity的onCreate中换回来。 这样启动时看到的就是你配置的logo图了。

具体操作一下:

        <activity android:name=".MainActivity" android:theme="@style/SplashTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
复制代码

这里我的而第一个activity是MainActivity,配置了theme是R.style.SplashTheme,来看下:

    <style name="SplashTheme" parent="AppNoActionBarAlphaAnimTheme">
        <item name="android:windowBackground">@drawable/splash_background</item>
    </style>
复制代码

看到 android:windowBackground已经配置成了自定义的drawable,这个就是关键点了,而默认是windowBackground是白色。看看自定义的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="@android:color/white"/>
    <item>
        <bitmap
            android:src="@drawable/dog"
            android:gravity="center"/>
    </item>

</layer-list>
复制代码

drawable的根节点是<layer-list>,然后一层是白色底,一层就是我们的logo图片了。

最后,在activity的onCreate中把Theme换回R.style.AppTheme即可(要在super.onCreate之前)。

    protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
    }
复制代码

效果如下:

可以看到,确实视觉上体验比白屏好很多。

但实际上启动速度并没有变快,下面就来看看可以真实提高启动速度的方案有哪些。

6.2 异步初始化

前面提到 提高启动速度,核心思想就是 减少主线程的耗时操作。启动过程中 可控住耗时的主线程 主要是Application的onCreate方法、Activity的onCreate、onStart、onResume方法。

通常我们会在Application的onCreate方法中进行较多的初始化操作,例如第三方库初始化,那么这一过程是就需要重点关注。

减少主线程耗时的方法,又可细分为异步初始化、延迟初始化,即把 主线程任务 放到子线程执行 或 延后执行。 下面就先来看看异步初始化是如何实现的。

执行异步请求,一般是使用线程池,例如:

        Runnable initTask = new Runnable() {
            @Override
            public void run() {
                //init task
            }
        };

        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadCount);
        fixedThreadPool.execute(initTask);
复制代码

但是通过线程池处理初始化任务的方式存在三个问题:

  • 代码不够优雅 假如我们有 100 个初始化任务,那像上面这样的代码就要写 100 遍,提交 100 次任务。
  • 无法限制在 onCreate 中完成 有的第三方库的初始化任务需要在 Application 的 onCreate 方法中执行完成,虽然可以用 CountDownLatch 实现等待,但是还是有点繁琐。
  • 无法实现存在依赖关系 有的初始化任务之间存在依赖关系,比如极光推送需要设备 ID,而 initDeviceId() 这个方法也是一个初始化任务。

那么解决方案是啥?启动器

LauncherStarter,即启动器,是针对这三个问题的解决方案,结合CountDownLatch对线程池的再封装,充分利用CPU多核,自动梳理任务顺序

使用方式:

  • 引入依赖
  • 划分任务,确认依赖和限制关系
  • 添加任务,执行启动

首先依赖引入:

implementation 'com.github.zeshaoaaa:LaunchStarter:0.0.1'
复制代码

然后把初始化任务划分成一个个任务;厘清依赖关系,例如任务2要依赖任务1完成后才能开始;还有例如3任务需要在onCreate方法结束前完成;任务4要在主线程执行。

然后添加这些任务,开始任务,设置等待。

具体使用也比较简单,代码如下:

public class MyApplication extends Application {

    private static final String TAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();

        TaskDispatcher.init(getBaseContext());
        TaskDispatcher taskDispatcher = TaskDispatcher.createInstance();

        // task2依赖task1;
        // task3未完成时taskDispatcher.await()处需要等待;
        // test4在主线程执行
        //每个任务都耗时一秒
        Task1 task1 = new Task1();
        Task2 task2 = new Task2();
        Task3 task3 = new Task3();
        Task4 task4Main = new Task4();

        taskDispatcher.addTask(task1);
        taskDispatcher.addTask(task2);
        taskDispatcher.addTask(task3);
        taskDispatcher.addTask(task4Main);

        Log.i(TAG, "onCreate: taskDispatcher.start()");
        taskDispatcher.start();//开始

        taskDispatcher.await();//等task3完成后才会往下走
        Log.i(TAG, "onCreate: end.");
    }

    private static class Task1 extends Task {
        @Override
        public void run() {
            Log.i(TAG, Thread.currentThread().getName()+" run start: task1");
            doTask();
            Log.i(TAG, Thread.currentThread().getName()+" run end: task1");
        }
    }

    private static class Task2 extends Task {
        @Override
        public void run() {
            Log.i(TAG, Thread.currentThread().getName()+" run start: task2");
            doTask();
            Log.i(TAG, Thread.currentThread().getName()+" run end: task2");
        }

        @Override
        public List<Class<? extends Task>> dependsOn() {
            //依赖task1,等task1执行完再执行
            ArrayList<Class<? extends Task>> classes = new ArrayList<>();
            classes.add(Task1.class);
            return classes;
        }
    }

    private static class Task3 extends Task {
        @Override
        public void run() {
            Log.i(TAG, Thread.currentThread().getName()+" run start: task3");
            doTask();
            Log.i(TAG, Thread.currentThread().getName()+" run end: task3");
        }

        @Override
        public boolean needWait() {
            //task3未完成时,在taskDispatcher.await()处需要等待。这里就是保证在onCreate结束前完成。
            return true;
        }
    }

    private static class Task4 extends MainTask {
    //继承自MainTask,即保证在主线程执行
        @Override
        public void run() {
            Log.i(TAG, Thread.currentThread().getName()+" run start: task4");
            doTask();
            Log.i(TAG, Thread.currentThread().getName()+" run end: task4");
        }
    }

    private static void doTask() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

有4个初始化任务,都耗时1秒,若都在主线程执行,那么会耗时4秒。这里使用启动器执行,并且保证了上面描述的任务要求限制。执行完成后日志如下:

2020-07-17 12:06:20.648 26324-26324/com.hfy.androidlearning I/MyApplication: onCreate: taskDispatcher.start()
2020-07-17 12:06:20.650 26324-26324/com.hfy.androidlearning I/MyApplication: main run start: task4
2020-07-17 12:06:20.651 26324-26427/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-1 run start: task1
2020-07-17 12:06:20.657 26324-26428/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-2 run start: task3

2020-07-17 12:06:21.689 26324-26324/com.hfy.androidlearning I/MyApplication: main run end: task4
2020-07-17 12:06:21.689 26324-26427/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-1 run end: task1
2020-07-17 12:06:21.690 26324-26429/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-3 run start: task2
2020-07-17 12:06:21.697 26324-26428/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-2 run end: task3
2020-07-17 12:06:21.697 26324-26324/com.hfy.androidlearning I/MyApplication: onCreate: end.

2020-07-17 12:06:22.729 26324-26429/com.hfy.androidlearning I/MyApplication: TaskDispatcherPool-1-Thread-3 run end: task2
复制代码

可见主线程耗时只有1秒。 另外,要注意的是,task3、task4一定是在onCreate内完成了,task1、task2都可能是在onCreate结束后一段时间才完成,所以在Activity中就不能使用task1、task2相关的库了。那么 在划分任务,确认依赖和限制关系时就要注意了。

异步初始化就说这么多,原理部分可直接阅读源码,很容易理解。接着看延迟初始化。

6.3 延迟初始化

在 Application 和 Activity 中可能存在优先级不高的初始化任务,可以考虑把这些任务进行 延迟初始化。延迟初始化并不是减少了主线程耗时,而是让耗时操作让位、让资源给UI绘制,将耗时的操作延迟到UI加载完毕后。

那么问题来了,如何延迟呢?

  • 使用new Handler().postDelay()方法、或者view.postDelay()——但是延迟时间不好把握,不知道啥时候UI加载完毕。
  • 使用View.getViewTreeObserver().addOnPreDrawListener()监听——可以保证view绘制完成,但是此时发生交互呢,例如用户在滑动列表,那么就会造成卡顿了。

那么解决方案是啥?延迟启动器

延迟启动器,利用IdleHandler特性,在CPU空闲时执行,对延迟任务进行分批初始化, 这样 执行时机明确、也缓解界面UI卡顿。 延迟启动器就是上面的LauncherStarter中的一个类。

public class DelayInitDispatcher {

    private Queue<Task> mDelayTasks = new LinkedList<>();

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll();
                new DispatchRunnable(task).run();
            }
            return !mDelayTasks.isEmpty();
        }
    };

    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }

    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }

}
复制代码

使用也很简单,例如在闪屏页中添加任务开始即可:

//SpalshActivity

DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();

protected void onCreate(Bundle savedInstanceState) {
        delayInitDispatcher.addTask(new Task() {
            @Override
            public void run() {
                Log.i(TAG, "run: delay task begin");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.i(TAG, "run: delay task end");
            }
        });
        delayInitDispatcher.start();
}
复制代码

经测试,确实是在是在布局展示后开始任务。但是如果耗时较长(例子中是3秒),过程中滑动屏幕,是不能及时响应的,会感觉到明显的卡顿。

所以,能异步的task优先使用异步启动器在Application的onCreate方法中加载,对于不能异步且耗时较少的task,我们可以利用延迟启动器进行加载。如果任务可以到用时再加载,可以使用懒加载的方式。

IdleHandler原理分析:

//MessageQueue.java
    Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }
复制代码

从消息队列取消息时,如果没有取到消息,就执行 空闲IdleHandler,执行完就remove。

6.4 Multidex预加载优化

安装或者升级后 首次 MultiDex 花费的时间过于漫长,我们需要进行Multidex的预加载优化。

5.0以上默认使用ART,在安装时已将Class.dex转换为oat文件了,无需优化,所以应判断只有在主进程及SDK 5.0以下才进行Multidex的预加载

抖音BoostMultiDex优化实践:

抖音BoostMultiDex优化实践:Android低版本上APP首次启动时间减少80%(一)

Github地址:BoostMultiDex

快速接入:

  • build.gradle的dependencies中添加依赖:
dependencies {
    // For specific version number, please refer to app demo
    implementation 'com.bytedance.boost_multidex:boost_multidex:1.0.1'
}
复制代码
  • 与官方MultiDex类似,在Application.attachBaseContext的最前面进行初始化即可:
public class YourApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        BoostMultiDex.install(base);
    }
复制代码

今日头条5.0以下,BoostMultiDex、MultiDex启动速度对比

6.5 页面数据预加载

闪屏页、首页的数据预加载:闪屏广告、首页数据 加载后缓存到本地,下次进入时直接读取缓存。 首页读取缓存到内存的操作还可以提前到闪屏页。

6.6 页面绘制优化

闪屏页与主页的绘制优化,这里涉及到绘制优化相关知识了,例如减少布局层级等。

七、总结

我们先介绍了启动流程、优化思想、耗时检测、分析工具,然后给出了常用优化方案:异步初始化、延迟初始化。涉及了很多新知识和工具,一些地方文章中没有展开,可以参考给出的连接详细学习。毕竟性能优化是多样技术知识的综合使用,需要系统掌握对应工作流程被、分析工具、解决方案,才能对性能进行深层次的优化。

好了,今天就到这里,欢迎留言讨论~

点关注,更多Android开发技能~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345