背景
在超市排队结账,扫码支付启动十几秒都还没完成,只能换一个工具支付?
目的
优化启动速度。
分析
1、启动过程分析
- T1 预览窗口显示
eg: 微信 Theme 属性创建的预览窗口。可以禁用或者透明,那么这段时间依然看到的是桌面。 - T2 闪屏显示
微信进程和闪屏页创建完毕,一些列 inflate view …准备工作后,看到 “小地球”。 - T3 主页显示
用户可以看到微信的主界面。 - T4 界面可操作
启动完成后,微信还有很多工作需要继续执行,eg:小程序框架和进程、聊天、朋友圈界面预加载…,完成后,用户真正可以进行聊天。
2、启动问题分析
4个阶段,大多3个问题。TOP3
- icon 点击很久无响应
Theme 禁用或者透明 导致。 - 首页显示太慢 即t3 显示
启动流程复杂,eg :闪屏广告、热修复框架、插件化框架、大前端框架、所有装备工作 init。 - 首页显示后无法操作
T3 展示慢,那么异步咋样?后果:首页白屏,或者首页后用户无法操作。
方案
启动优化方法与卡顿优化基本相同。
因为太重要。4个阶段
定位问题工具,分析工具
- 工具选择:
需要找性能损耗小,而且适用性广的工具。
Traceview 性能损耗太大
Nanoscope 暂时只支持 Nexus 6P 和 x86 模拟器,无法针对中低端机
Simpleperf 的火焰图并不适合做启动流程分析
systrace 可以很方便地追踪关键系统调用的耗时情况,但是不支持应用程序代码的耗时分析
选择:systrace + 函数插桩
它还可以看到系统的一些关键事件,例如 GC、System Server、CPU 调度等。
通过插桩,我们可以看到应用主线程和其他线程的函数调用流程。它的实现原理非常简单,就是将下面的两个函数分别插入到每个方法的入口和出口。
class Trace {
public static void i(String tag) {
Trace.beginSection(name);
}
public static void o() {
Trace.endSection();
}
}
class Test {
public void test() {
Trace.i("Test.test()");
// 原来的工作
Trace.o();
}
}
只有准确的数据评估才能指引优化的方向,这一步是非常非常重要的。
优化方式
前提:
在拿到整个启动流程的全景图之后,我们可以清楚地看到这段时间内系统、应用各个进程和线程的运行情况,现在我们要开始真正开始“干活”了。
具体的优化方式:
闪屏优化、业务梳理、业务优化、线程优化、GC 优化、系统调用优化。
-
闪屏优化
即T2优化,Theme 页后的显示。预览闪屏???待查,今日头条采用的“预览闪屏”
我比较推荐的做法是,只在 Android 6.0 或者 Android 7.0 以上才启用“预览闪屏”方案,让手机性能好的用户可以有更好的体验。微信做的另外一个优化是合并闪屏和主页面的 Activity,减少一个 Activity 会给线上带来 100 毫秒左右的优化。
缺点:管理时会非常复杂,特别是有很多例如 PWA、扫一扫这样的第三方启动流程的时候。 -
业务梳理
砍掉无用,懒加载选择
启动过程中,哪些是必须的、哪些是可以砍掉的、哪些是可以懒加载的,哪些是可以根据业务场景来决定不同的启动模式(eg,扫一扫启动只需要加载几个模块即可)。低端机,砍掉功能取舍。降级功能。 -
业务优化
梳理后剩下的都是启动必须的模块。
抓大放小,优化耗时长的步骤。一些架构、各种回调集中执行,初始化耗时长。
各种反射、Hook,这些历史包袱需要逐渐有勇气的去偿还。 -
线程优化
优化各个线程像填空题和解锁提一样,希望利用上所有碎片时间。
因此主线程和各个线程一直都是满载的。线程优化,主要在于:
减少 CPU 调度带来的波动,让应用的启动时间更加稳定。具体做法:
控制线程数量。防止太多线程竞争CPU。
因此,统一线程池,根据机器适配。
proc/[pid]/sched:
nr_voluntary_switches:
主动上下文切换次数,因为线程无法获取所需资源导致上下文切换,最普遍的是 IO。
nr_involuntary_switches:
被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占 CPU。
通过 systrace 可以看到锁等待的事件,我们需要排查这些等待是否可以优化,特别是防止主线程出现长时间的空转。
- 很多启动框架,会使用 Pipeline 机制,根据业务优先级规定业务初始化时机。
微信内部使用的 mmkernel、阿里最近开源的 Alpha 启动框架,它们为各个任务建立依赖关系,最终构成一个有向无环图。
对于可以并发的任务,会通过线程池最大程度提升启动速度。如果任务的依赖关系没有配置好,很容易出现下图这种情况。
- GC 优化
在启动过程,要尽量减少 GC 的次数。 - 系统调用优化
- 影响
I/O 优化
启动过程读了什么文件、多少个字节、Buffer 是多大、使用了多长时间、在什么线程等一系列信息。
SharedPreference
在初始化的时候还是要全部数据一起解析。如果它的数据量超过 1000 条,启动过程解析时间可能就超过 100 毫秒。
类重排
Dex 文件用的到的类和安装包 APK 里面各种资源文件一般都比较小,但是读取非常频繁。我们可以利用系统这个机制将它们按照读取顺序重新排列,减少真实的磁盘 I/O 次数。
启动过程类加载顺序可以通过复写 ClassLoader 得到。
class GetClassLoader extends PathClassLoader {
public Class<?> findClass(String name) {
// 将 name 记录到文件
writeToFile(name,"coldstart_classes.txt");
return super.findClass(name);
}
}
然后通过 ReDex 的Interdex调整类在 Dex 中的排列顺序,最后可以利用 010 Editor 查看修改后的效果。
资源文件重排
Facebook :“资源热图”相对比较完善,也建设了一些配套的Dashboard 工具,希望后续可以开源出来。
支付宝:https://mp.weixin.qq.com/s/79tAFx6zi3JRG-ewoapIVQ
- 统计
来确定,加载顺序,得到顺序列表。 - 度量
重排后,是否生效了。 - 自动化
任何代码提交都有可能改变启动过程中类和资源的加载顺序。
如果完全依靠人工手动处理,这个事情很难持续下去。
通过定制 ROM 的一些埋点和配合的工具,我们可以将它们放到自动化流程当中。
方法:
调整安装包文件排列需要修改 7zip 源码实现支持传入文件列表顺序,同样最后可以利用 010 Editor 查看修改后的效果。
这两个优化可能会带来 100~200 毫秒的提高
我们还可以大大减少启动过程 I/O 的时间波动。
黑科技慎用
Tinker 框架在加载补丁后,应用启动速度会降低 5%~10%
应用加固对启动速度来说简直是灾难。
参考文章
极客时间:
https://time.geekbang.org/column/article/0?cid=142
https://time.geekbang.org/column/article/74044