应用启动耗时优化

应用启动耗时优化

【案 例 摘 要】


案例背景


应用启动优化是应用工程师在开发应用时所必须要面对的话题。更快的应用启动,往往带来更好的用户体验。相反,如果应用启动过慢,会给人一种非常糟糕的使用体验。甚至会让用户直接进行卸载。应用启动耗时,除了跟平台资源(如内存、cpu 性能等)有较大关系之外,还跟我们应用自身的实现有很大关系。本文就站在应用开发者的角度,来介绍启动优化的思路、原理以及方法。帮助应用工程师写出更高质量的APP 。

目标


通过该案例帮助大家理解和掌握以下内容:
1、启动耗时的指标
2、应用启动过程与优化原理
3、黑白屏优化及原理
4、多任务初始化启动框架的设计
5、A S 启动分析工具的使用方法


解决方案


1、应用启动状态与耗时指标


应用启动根据启动过程不同,分为冷启动、热启动和温启动三种状态。
冷启动:应用从最开始启动,需要创建应用进程,通常是设备重启应用首次启动,或者应用被杀后首次启动;
热启动: 已经启动的应用,从后台再次进入前台被用户可见。这个过程应用始终驻留在内存中,启动过程不必执行对象初始化、加载布局等。
温启动:它的开销介于冷启动和热启动之间。退出后进程没有被销毁,但是回到前台又需要做冷启动过程的部分操作。如,用户在瑞出应用后再重新启动,虽然进程没有被销毁,但是仍然要执行onCreate 重新创建Activity 。
应用启动时间从用户角度看,是从点击开始,到第一帧画面显示出来的时间。通常情况下热启动耗时超过1.5s 、温启动超过2s 、冷启动超过5s5s,被认为启动耗时过长。


启动时间测量
应用启动时间除了用户用肉眼感知。比较常用的测量方法是使用高速摄影机录制整个开机过程,通过后期逐帧播放,数出启动过程一共经过多少帧,然后根据帧率计算出启动时间。多次测量取平均值。
站在开发者的角度可以使用系统统计日志和adbadb命令的方式进行统计。
系统日志:在Android4.4(API19)Android4.4(API19)及更高版本中,应用启动完成,logcatlogcat会输出一行包括应用包名和“DisplayedDisplayed”关键字日志,如,
“ActivityManager: Displayed com.android.myDemo/.MyActivity: +3s500ms
后面的时间代表启动进程到在屏幕上完成ActivityActivity第一帧画面绘制所用的时间。
adb命令统计方式:使用”adb shell am start adb shell am start –S S –W xxx” 启动窗口。

-W, 表示在启动应用时要等待activityactivity启动完成。

-S,表示在启动应用前要对它进行强行停止,然后再进行启动。
如“adb shell am start adb shell am start -S -W com.android.myDemo/.MainActivity
窗口启动后,命令行返回ThisTimeThisTime、TotalTimeTotalTime、WaitTimeWaitTime几个统计数据。
其中,ThisTime表示一连串ActActitityitity启动,最后一个AActivityctivity启动的耗时;
To
TotalTimetalTime表示新应用窗口启动时间,包括创建进程和活动窗口的启动,如果启动单个窗口TThisTimehisTime应该等于TTotalTimeotalTime;
W
WaitTimeaitTime表示前一个窗口ppuaseuase和当前新应用启动的时间;
通常常情况下WaitTime > TotalTime >= ThisTime。


2、应用启动过程与优化原理


我们先了解下应用启动主要流程,以及各个阶段所主要做的工作。Android版本虽然不断升级迭代,但是应用启动流程的主要步骤变化却是比较小的。这里及下面说的启动,指的是应用冷启动。

上面图就是应用启动的主要步骤,我们将它分为三个阶段
第一阶段:Launcher 启动应用
点击桌面应用图标,与AMS 进行通讯,创建应用进程,启动应用。
这里只讨论在应用开发者角度的启动优化方案。这一阶段的工作在系统层
次,主要用Framework 工程师完成。应用开发者什么也做不了。
第二阶段:Application 的执行阶段,主要执行初始化ContentProvider、初
始化PhoneWindow,以及上下文Context 等。这一阶段主要是系统帮我们做的
工作,应用过程师同样做不了什么。不过,这一阶段可以通过一些配置影响用
户的感知。经常听到的“黑白屏”优化,就是这一阶段做的工作。
“黑白屏”优化
在安卓系统中是Window 负责显示,我们看到的应用界面实际上就是
PhoneWindow。而安卓里面Application 是没有Window 的,只有Activity 才有
Window。在此时的“第二阶段”,Activity 还没有被创建出来,也就是没有
Window,那这个时候手机上显示的是什么呢?这个就是安卓系统自动帮我们临
时创建的Window,这个Window 默认是白色或黑色的背景,因而又叫做黑白
屏。这个Window 是这这里进行PhoneWindowManager 的addSplashScreen 方
法进行添加。调用流程如下:
(下面流程是基于Android-11 即SDK30 分析的,最新的SDK 流程有变
化,不过思路是一样的。)

重点看一下addSplashScreen 的方法:
首先会new 一个PhoneWindow 出来,就是我们前面说的临时Window,

然后调用addSplashscreenContent 方法来创建一个用来显示的View,就是黑白屏。最后调用WindowManager 的addView 将它显示出来。

我们单独看一下addSplashscreenContent 方法。这个方法很简单,就是将一个配置的资源转换成drawable 再添加到view 上。

我们看到R.styleable.Window_windowSplashscreenContent 这一个属性。
因此我们可以通过配置该属性让在第一个窗口显示之前,显示我们想要的画面。这个属性如果没有配置就是默认的值:

这个属性可以在themetheme中进行配置:

这就是“黑白屏”优化的原理,开发者通过设置一个和应用主题差不多的主题背景,在启动时,第一个窗口没有显示之前,就这个背景显示出来。让用户误以为已经进入应用。达到启动“优化”的目的。
当然,这种优化实际上是一种障眼法、是“欺骗”的方式。


第三阶段:启动应用窗口,这一阶段主要执行application,以及activity的生命周期函数。
在这个阶段会加载应用的Window,将用户窗口正真显示出来。这个阶段有两个主要的过程执行application生命周期执行MainActivity生命周期
application生命周期中主要关注它的onCreate方法,开发者往往在这个阶段执行各种环境、依赖sdk等的初始化工作。
通常开发者会通过多线程方式来加快这一进程。实际上这种做法并不十分有效。因为过多线程,会导致cpu花大量的时间在线程切换的开销上。
后面章节详细介绍一种启动框架的方法设计,以最优的方式处理多任务初始化的工作。
MainAcivity生命周期,主要经历attach、onCreate、onStart、onResumee几个过程。
在attach 阶段主要绑定上下文Context、创建Window(就是PhoneWindow)
为window 创建WindowManager 等。
在onCreate 阶段主要就是执行setContentView 方法,从布局文件(XML 文件)解析出View 树。在这一阶段,开发者能做的就是尽可能减少布局的层次。因为这里是采用inflate 的方式,解析布局文件,并利用反射来创建View,这个过程是十分耗时的。因此通过简化布局,减少布局嵌套层次,能够有有效加快这个过程。这里可以选择使用约束布局取代常用的线性和框架布局,据Google官方数据显示,约束布局渲染速度比线性和框架布局提高大约40%
在onResume 阶段,这一阶段会进行wm.addView 操作,将上一步创建的view 树添加到window 上。此时会将在第二阶段window 上添加的黑白屏覆盖掉。到这里我们第一个应用界面才算正真的显示出来。
因此,应用开发者能够进行启动优化的地方就是在第三阶段进行。这一阶段刚好是在WM 添加临时的黑白屏和添加第一个用户界面之间。在这一阶段应尽可能减少耗时的操作,尽量将非必要的工作,放到这个阶段之外进行
这里需要注意的是ActivityThread 是先回调的Activity onResume 方法,再进行wm.addView 操作。因此在执行onResume 生命周期时,window 还没有显示出来。可以在onReume 函数里post 一个事件来执行耗时的操作,这样就不会阻碍启动流程。

我们可能在启动过程种,通常在application 的onCreate 方法,不得不进行一些初始化相关的工作。下面重点讲一下多任务启动框架的设计和实现,让这一过程达到最优解,尽可能压缩启动时的消耗。


3、多初始化任务启动框架设计


这一节重点讲解启动框架的设计思路和核心部分的实现。让启动过程多任务初始化工作按最优的方式进行,解决在应用启动阶段需要执行多个初始化任务耗时的问题。


1)确定任务执行顺序
根据任务之间的依赖关系,建立有向无环图(DAG 图)。根据DAG 图确定任务执行顺序。
假设现在有五个任务,他们之间的依赖关系是:Task5 依赖Task4 和Task3,Task4 依赖Task2,Task3 依赖Task1,Task2 也依赖Task1。
由此建立任务间依赖关系的DAG 图如下所示:

对于DAG 图,使用拓扑排序,就可以得到一个排序。这就是任务的执行顺序。
我们认为依赖的任务为入度,被依赖的为出度。即上图中箭头出去的为出度,被箭头指向的为节点的入度。
拓扑排序的算法,简单描述就是:
找出一个入度为0 的顶点;
记录该顶点后,将其删除;
循环执行上面步骤,直到所有顶点都被删除。被依次记录的顶点就是拓扑排序的结果。
当然,拓扑排序的结果并不唯一。
如上图的排序可以是:Task1、Task2、Task4、Task3、Task5 也可以是Task1、Task2、Task3、Task4、Task5。这两个排序都是任务可以进行的执行顺序。


2)并行执行任务处理
如上图所示,Task1 执行完成后,Task2 和Task3 是可以并行执行的。这样能够更大程度的加速执行过程。在上一步的基础上,我们引入CountdownLatch(闭锁)工具,来解决这一问题。
CountdownLatch 它的底层是使用CAS 来实现的,简单来说,就是用原子的方式对一个变量执行减法。当一个线程等待在闭锁上,会不断轮询这个变量,直到这个变量变为0 后,才继续进行后面的操作。而其他的线程在执行完或者执行到某一步骤后可以对该变量进行减操作。当该变量变为0 时,等待在
该闭锁的线程就可以继续执行了。有兴趣的可以进一步查阅它的实现原理。
通过任务依赖数以及依赖关系初始化每个任务的闭锁。
将这些任务同时启动起来,闭锁计数为0 的可以直接执行,当执行完成后,将被它所依赖的任务的闭锁计数减1,当一个任务的闭锁计数为0 就可以自动执行起来,如此往复循环。所有任务可以高效率的执行完毕。

上图就是加入闭锁之后,Task1~Task5 的执行情况。


3)子线程、主线程任务区别处理
实际初始化时,可能会遇到有些任务需要在主线程执行的情形。因此,这里进一步完善框架,将主线程和子线程处理的任务进行区分。在初始化任务时,指定“是否需要在主线程执行”的标志。实际执行中,判断该标志。为true,在主线程执行;为false 丢到线程池执行。
基于上面1~3设计的思考,给出核心实现代码。读者可以自行进行完善,可以根据自己需要增加如设置线程优先级、统计启动耗时等特性。

4、应用启动耗时分析工具


Android Studio提供了CPU Profile工具,可以帮助我们分析应用启动中的耗时问题。这个工具就是可以记录CPU的活动,我们在应用启动时开启,就可以跟踪应用启动过程中的活动,进而可以用它来辅助分析启动耗时问题。
工具使用方法:
a、依次选择工具栏上 RunRun –> E> Edidit Ct Cononfigurationsfigurations
b、在PrProfilingofiling标签中,勾选StartStart recording CPU activity on startuprecording CPU activity on startup复选框这里有四个选项可以选择,它是用来配置CCPUPU采样的方法:
Java/Kotlin Method Sample (legacy),以采样的方式捕获jjava/kotlinava/kotlin代码执行调用的堆栈。这种方式产生的开销较小,但是会出现漏采的问题。
Java/Kotlin Methods Trace, , 追踪方式捕获javajava/kotlin/kotlin代码调用堆栈,所有执行的javajava/kotlin/kotlin方法都会被记录,因而产生的开销比较大。
Callstack Sample,捕获应用线程采样数据。
System Trace,跟踪系统调用,主要是应用与系统资源的交互情况。如,线程的状态、执行时间、cpu的状态等。

c、选择好采用方法后,点击 Apply
d、依次选择Run –> Profile可以看到下方出现CPU采用数据
e、点击Record”开始采样,启动应用
f、应用启动完成,点击Stop结束采样,即可获得采样结果。
以上操作直接用AS就可以看到,不截图展示了。
g、对采样结果进行分析
AS根据采样结果可以为我们生成三种视图,分别是Call Chart、Flame Chat、Top Down Tree、Bottom Up Tree。
Flame Chat:这是一个倒置的调用图表,横轴表示方法执行的长短,越长表示执行时间越长。向上表示调用的层次,即下方的方法,调用执行上面的方法,因此图形越像上越窄,看上去像火焰的形状,因而也被称为“火焰图”。通过这种图形化的方式,可以很直观的看到一个方法的调用堆栈,以及每一个方法执行时间的长短。

Top Down:
这个图示,实际上是一个调用顺序表,最上面的最顶层的函数,向下是它调用的方法,这个表会统计每个函数执行的时间,执行占用百分比等。通过它可以看到更详细的执行时间统计。
Bottom Up:
这个图形和Top Down刚好相反,它是最上面是最底层的函数,点开查看它的下一节点表示调用它的函数。通过这个图标查看每个函数会被哪些函数调用,以及调用执行的时间。

通常定位到耗时的点,在到对应的代码逻辑里尝试进行有针对性的进行优化。


结果


通过使用“黑白屏”优化增加用户体验、使用设计良好的多任务初始化框架优化初始化过程、使用约束布局减少界面渲染开销、使用延迟加载方案将耗时操作放到应用启来之后进行,以及使用CPU Profile工具精确定位耗时点并有针对性优化等一系列方案。可以有效加速应用启动过程。


反思与启示


更快的应用启动速度,往往收获更好的用户体验,也更加容易留住用户。应用启动有很大一部分在系统测进行,站在应用工程师的角度,仿佛什么也做不了。但是我们还是要了解应用启动的过程,帮助我们更好的理解和处理应用启动耗时相关的问题,以及帮助我们在日常开发中写出更加高效的应用程序。