启动优化(上)
从用户点击图标开始,整个启动过程经过哪几个关键阶段,又会给用户带来哪些体验问题。
-
启动过程分析
image.png
我以微信为例,用户从桌面点击图标开始,会经过 4 个关键阶段
1.T1 预览窗口显示。系统在拉起微信进程之前,会先根据微信的 Theme 属性创建预览窗口。当然如果我们禁用预览窗口或者将预览窗口指定为透明,用户在这段时间依然看到的是桌面。
2.T2 闪屏显示。在微信进程和闪屏窗口页面创建完毕,并且完成一系列 inflate view、onmeasure、onlayout 等准备工作后,用户终于可以看到熟悉的“小地球”。
3.T3 主页显示。在完成主窗口创建和页面显示的准备工作后,用户可以看到微信的主界面。
4.T4 界面可操作。在启动完成后,微信会有比较多的工作需要继续执行,例如聊天和朋友圈界面的预加载、小程序框架和进程的准备等。在这些工作完成后,用户才可以真正开始愉快地聊天。
2、启动问题分析
从启动流程的 4 个关键阶段,我们可以推测出用户启动过程会遇到的 3 个问题。这 3 个问题其实也是大多数应用在启动时可能会遇到的。
问题 1:点击图标很久都不响应
如果我们禁用了预览窗口或者指定了透明的皮肤,那用户点击了图标之后,需要 T2 时间才能真正看到应用闪屏。对于用户体验来说,点击了图标,过了几秒还是停留在桌面,看起来就像没有点击成功,这在中低端机中更加明显。
问题 2:首页显示太慢
现在应用启动流程越来越复杂,闪屏广告、热修复框架、插件化框架、大前端框架,所有准备工作都需要集中在启动阶段完成。上面说的 T3 首页显示时间对于中低端机来说简直就是噩梦,经常会达到十几秒的时间。
问题 3:首页显示后无法操作。
既然首页显示那么慢,那我能不能把尽量多的工作都通过异步化延后执行呢?很多应用的确就是这么做的,但这会造成两种后果:要么首页会出现白屏,要么首页出来后用户根本无法操作。
启动优化
启动速度优化的方法和卡顿优化基本相同,不过因为启动实在是太重要了,我们会更加“精打细算”。我们希望启动期间加载的每个功能和业务都是必须的,它们的实现都是经过“千锤百炼”的,特别是在中低端机上面的表现。
- 优化工具
我们可以通过下面的命令,可以查看手机支持哪些 systrace 类型。不同的系统支持的类型有所差别,其中 Dalvik、sched、ss、app 都是我们比较关心的
python systrace.py --list-categories
通过插桩,我们可以看到应用主线程和其他线程的函数调用流程。它的实现原理非常简单,就是将下面的两个函数分别插入到每个方法的入口和出口。
class Trace {
public static void i(String tag) {
Trace.beginSection(name);
}
public static void o() {
Trace.endSection();
}
}
- 优化方式
具体的优化方式,我把它们分为闪屏优化、业务梳理、业务优化、线程优化、GC 优化和系统调用优化。
闪屏优化
今日头条把预览窗口实现成闪屏的效果,这样用户只需要很短的时间就可以看到“预览闪屏”。这种完全“跟手”的感觉在高端机上体验非常好,但对于中低端机,会把总的的闪屏时间变得更长。
如果点击图标没有响应,用户主观上会认为是手机系统响应比较慢。所以我比较推荐的做法是,只在 Android 6.0 或者 Android 7.0 以上才启用“预览闪屏”方案,让手机性能好的用户可以有更好的体验。
微信做的另外一个优化是合并闪屏和主页面的 Activity,减少一个 Activity 会给线上带来 100 毫秒左右的优化。但是如果这样做的话,管理时会非常复杂,特别是有很多例如 PWA、扫一扫这样的第三方启动流程的时候。
业务梳理
我们首先需要梳理清楚当前启动过程正在运行的每一个模块,哪些是一定需要的、哪些可以砍掉、哪些可以懒加载。我们也可以根据业务场景来决定不同的启动模式,例如通过扫一扫启动只需要加载需要的几个模块即可。对于中低端机器,我们要学会降级,学会推动产品经理做一些功能取舍。但是需要注意的是,懒加载要防止集中化,否则容易出现首页显示后用户无法操作的情形。
业务优化
通过梳理之后,剩下的都是启动过程一定要用的模块。这个时候,我们只能硬着头皮去做进一步的优化。优化前期需要“抓大放小”,先看看主线程究竟慢在哪里。最理想是通过算法进行优化,例如一个数据解密操作需要 1 秒,通过算法优化之后变成 10 毫秒。退而求其次,我们要考虑这些任务是不是可以通过异步线程预加载实现,但需要注意的是过多的线程预加载会让我们的逻辑变得更加复杂。
比较常见的是一些事件会被各个业务模块监听,大量的回调导致很多工作集中执行,部分框架初始化“太厚”,例如一些插件化框架,启动过程各种反射、各种 Hook,整个耗时至少几百毫秒
线程优化
线程的优化主要在于减少 CPU 调度带来的波动,让应用的启动时间更加稳定,从具体的做法来看,线程的优化一方面是控制线程数量,线程数量太多会相互竞争 CPU 资源,因此要有统一的线程池,并且根据机器性能来控制数量。
GC 优化
在启动过程,要尽量减少 GC 的次数,避免造成主线程长时间的卡顿,特别是对 Dalvik 来说,我们可以通过 systrace 单独查看整个启动过程 GC 的时间
python systrace.py dalvik -b 90960 -a com.sample.gc
对于 GC 各个事件的具体含义,你可以参考https://developer.android.com/studio/profile/investigate-ram?hl=zh-cn。
image.png如果我们发现主线程出现比较多的 GC 同步等待,那就需要通过 Allocation 工具做进一步的分析。启动过程避免进行大量的字符串操作,特别是序列化跟反序列化过程。一些频繁创建的对象,例如网络库和图片库中的 Byte 数组、Buffer 可以复用。如果一些模块实在需要频繁创建对象,可以考虑移到 Native 实现。
在启动过程也不要过早地拉起应用的其他进程,System Server 和新的进程都会竞争 CPU 资源。特别是系统内存不足的时候,当我们拉起一个新的进程,可能会成为“压死骆驼的最后一根稻草”。它可能会触发系统的 low memory killer 机制,导致系统杀死和拉起(保活)大量的进程,从而影响前台进程的 CPU。