启动过程
如图所示
- T1 预览窗口显示。系统在拉起进程之前,会先根据Application的 Theme 属性创建预览窗口。当然如果我们禁用预览窗口或者将预览窗口指定为透明, 用户在这段时间依然看到的是桌面。
- T2 闪屏显示。在进程和闪屏窗口页面创建完毕,并且完成一系列 inflate view、onmeasure、onlayout 等准备工作后,用户终于可以看到启动Activity。
- T3 主页显示。在完成主窗口创建和页面显示的准备工作后,用户可以看到应用的主界面。
- T4 界面可操作。在启动完成后,会有比较多的工作需要继续执行,例如界面的预加载、小程序框架和进程的准备等。在这些工作完成后,用户才可以真正开始愉快地操作APP。
App启动过程
在冷启动的过程中,首先会通过 AMS 在 System 进程展示一个 Starting Window (通常情况下是个白屏,可以通过设置 Application 的 theme 修改),接着AMS会通过Zygote创建应用程序的进程,并通过一系列的步骤后调用 Application 的 attachBaseContext()、onCreate() 然后最终调用 Activity 的 onCreate() 以及进行View相关的初始化工作。在Activity展示出来后会替换掉之前的 Starting Window,这样启动过程结束。
启动时长
- 计算从 Application 的 attachBaseContext 方法开始到 第一个Activity 的 onWindowFocusChanged() 方法之间的时间,这样就可以初步得到应用的冷启动时间
- 在 Logcat 控制台搜索 Displayed 这个 tag ,就可以看到所有 App 的启动时长
- 通过Adb命令获取启动时长
adb shell am start -S -R 5 -W {ApplicationId}/{入口Activity全路径: com.chaitai.LauncherActivity}
-S:表示每次启动前先强行停止
-R:表示重复测试次数 - Trace 获取启动时长 (不推荐,推荐 Profiler)
1 2 3 4
//开始 Debug.startMethodTracing("onCreate") //结束 Debug.stopMethodTracing()
生成的 trace 文件的路径: /storage/emulated/0/Android/data/com.chaitai.crm/files/onCreate.trace
adb pull 方式导出文件,需要 root 权限
如果没有 root 权限,可以通过手机文件系统导出文件
打开 Profiler 工具点击加号选择“load from file…” 选择 xxx.trace 文件 - Profiler获取启动时长(推荐)
- 打开 Edit configurations…
- 点击 Profiling 标签
- 勾选 Start this recording on startup
- 点击 Run按钮右边的 Profile 按钮
- 运行到界面展示完成后点击 stop record
常见问题
- 点击图标很久都不响应 如果我们禁用了预览窗口或者指定了透明的皮肤,那用户点击了图标之后,需要 T2 时间才能真正看到应用闪屏。对于用户体验来说,点击了图标,过了几秒还是停留在桌面,看起来就像没有点击成功,这在中低端机中更加明显。
- 首页显示太慢 现在应用启动流程越来越复杂,闪屏广告、热修复框架、插件化框架、大前端框架,所有准备工作都需要集中在启动阶段完成。上面说的 T3 首页显示时间对于中低端机来说简直就是噩梦,经常会达到十几秒的时间。
- 首页显示后无法操作。 既然首页显示那么慢,那我能不能把尽量多的工作都通过异步化延后执行呢?很多应用的确就是这么做的,但这会造成两种后果:要么首页会出现白屏,要么首页出来后用户根本无法操作。 很多应用把启动结束时间的统计放到首页刚出现的时候,这对用户是不负责任的。看到一个首页,但是停住十几秒都不能滑动,这对用户来说完全没有意义。启动优化不能过于 KPI 化,要从用户的真实体验出发,要着眼从点击图标到用户可操作的整个过程。
android:windowDisablePreview
TODO windowDisablePreview顾名思义即取消窗体预览,会导致启动页在绘制完成后才显示,会感觉响应迟钝。方案2中手动设置了窗口背景图
具体优化内容
具体的优化方式,我把它们分为闪屏优化、业务梳理、业务优化、线程优化、GC 优化和系统调用优化。
- 闪屏优化
要解决闪屏优化,就要明白为什么冷启动时为什么会有一段时间的白屏或者黑屏。
这是因为冷启动时首先展示的不是Acitivity而是一个 “Starting Window” 这个Window 可以通过设置 可以通过设置 Application 的 theme 的 android:windowBackground 修改
今日头条把预览窗口实现成闪屏的效果,这样用户只需要很短的时间就可以看到“预览闪屏”。这种完全“跟手”的感觉在高端机上体验非常好,但对于中低端机,会把总的的闪屏时间变得更长。 如果点击图标没有响应,用户主观上会认为是手机系统响应比较慢。所以我比较推荐的做法是,只在 Android 6.0 或者 Android 7.0 以上才启用“预览闪屏”方案,让手机性能好的用户可以有更好的体验。 微信做的另外一个优化是合并闪屏和主页面的 Activity,减少一个 Activity 会给线上带来 100 毫秒左右的优化。但是如果这样做的话,管理时会非常复杂,特别是有很多例如 PWA、扫一扫这样的第三方启动流程的时候。 - 业务梳理 我们首先需要梳理清楚当前启动过程正在运行的每一个模块,哪些是一定需要的、哪些可以砍掉、哪些可以懒加载。我们也可以根据业务场景来决定不同的启动模式,例如通过扫一扫启动只需要加载需要的几个模块即可。对于中低端机器,我们要学会降级,学会推动产品经理做一些功能取舍。但是需要注意的是,懒加载要防止集中化,否则容易出现首页显示后用户无法操作的情形。
- 业务优化 通过梳理之后,剩下的都是启动过程一定要用的模块。这个时候,我们只能硬着头皮去做进一步的优化。优化前期需要“抓大放小”,先看看主线程究竟慢在哪里。最理想是通过算法进行优化,例如一个数据解密操作需要 1 秒,通过算法优化之后变成 10 毫秒。退而求其次,我们要考虑这些任务是不是可以通过异步线程预加载实现,但需要注意的是过多的线程预加载会让我们的逻辑变得更加复杂。 业务优化做到后面,会发现一些架构和历史包袱会拖累我们前进的步伐。比较常见的是一些事件会被各个业务模块监听,大量的回调导致很多工作集中执行,部分框架初始化“太厚”,例如一些插件化框架,启动过程各种反射、各种 Hook,整个耗时至少几百毫秒。还有一些历史包袱又非常沉重,而且“牵一发动全身”,改动风险比较大。但是我想说,如果有合适的时机,我们依然需要勇敢去偿还这些“历史债务”。
- 线程优化 很多启动框架,会使用 Pipeline 机制,根据业务优先级规定业务初始化时机。比如微信内部使用的mmkernel、阿里最近开源的Alpha启动框架,它们为各个任务建立依赖关系,最终构成一个有向无环图。对于可以并发的任务,会通过线程池最大程度提升启动速度。如果任务的依赖关系没有配置好,很容易出现下图这种情况,即主线程会一直等待 taskC 结束,空转 2950 毫秒。
- 系统调用优化 通过 systrace 的 System Service 类型,我们可以看到启动过程 System Server 的 CPU 工作情况。在启动过程,我们尽量不要做系统调用,例如 PackageManagerService 操作、Binder 调用等待。 在启动过程也不要过早地拉起应用的其他进程,System Server 和新的进程都会竞争 CPU 资源。特别是系统内存不足的时候,当我们拉起一个新的进程,可能会成为“压死骆驼的最后一根稻草”。它可能会触发系统的 low memory killer 机制,导致系统杀死和拉起(保活)大量的进程,从而影响前台进程的 CPU。 讲个实践的案例,之前我们的一个程序在启动过程会拉起下载和视频播放进程,改为按需拉起后,线上启动时间提高了 3%,对于 1GB 以下的低端机优化,整个启动时间可以优化 5%~8%,效果还是非常明显的。
- 保活 保活可以减少 Application 创建跟初始化的时间,让冷启动变成温启动。不过在 Target 26 之后,保活的确变得越来越难。 微信的 Hardcoder 方案和 OPPO 推出的Hyper Boost方案。根据 OPPO 的数据,对于手机 QQ、淘宝、微信启动场景会直接有 20% 以上的优化。 有的时候你问为什么微信可以保活?为什么它可以运行的那么流畅?这里可能不仅仅是技术上的问题,当应用体量足够大,就可以倒逼厂商去专门为它们做优化。 在App的主页面重写返回操作改为退出到后台,而不是关闭 Activity
- 线上监控 Android Vitals可以对应用冷启动、温启动时间做监控。 事实上,每个应用启动的流程都非常复杂,上面的图并不能真实反映每个应用的启动耗时。启动耗时的计算需要考虑非常多的细节,比如: 启动结束的统计时机。是否是使用用户真正可以操作的时间作为启动结束的时间。 启动时间扣除的逻辑。闪屏、广告和新手引导这些时间都应该从启动时间里扣除。 启动排除逻辑。Broadcast、Server 拉起,启动过程进入后台这些都需要排除出统计。 经过精密的扣除和排除逻辑,我们最终可以得到用户的线上启动耗时。正如我在上一期所说的,准确的启动耗时统计是非常重要的。有很多优化在实验室完成之后,还需要在线上灰度验证效果。这个前提是启动统计是准确的,整个效果评估是真实的。
StrictMode
通过StrictMode参数设置,找到主线程调用网络请求和IO等非法操作
Application的创建以及初始化流程
LoadedApk.makeApplication
1
2
3
4
5
6
7
8
9
10
11
12
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
instrumentation.callApplicationOnCreate(app);
return app;
}
Instrumentation.newApplication
1
2
3
4
5
6
7
8
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = getFactory(context.getPackageName())
.instantiateApplication(cl, className);
app.attach(context);
return app;
}
Application.attach
1
2
3
4
final void attach(Context context) {
attachBaseContext(context);
mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}
由代码可知 attachBaseContext 是在 onCreate 之前被调用的