序、众所周知,每一次开始写这种框架系类的文章的时候,就是我在找工作了,有闲暇时间可以写点东西。哈哈哈哈,26年找工作,简直就是。。。。。
重要的事情说三遍
有岗位的请 CALL 我,谢谢大家了。。。。
有岗位的请 CALL 我,谢谢大家了。。。。
有岗位的请 CALL 我,谢谢大家了。。。。
上一篇我们梳理了一次 Android 冷启动的大致过程:
Launcher → Zygote → ActivityThread → Application → Activity → 首帧显示。
知道启动过程之后,下一个问题就是:
我的 App 启动到底慢不慢?
启动优化不能靠"感觉"。
比如我们把一个 SDK 延迟初始化之后,感觉页面好像出来得更快了,但到底快了多少?是快了 20ms,还是 200ms?
如果没有统一的测量方式,优化前后的结果就无法比较。
所以启动优化真正开始之前,需要先建立:
启动性能基线。
一、启动速度到底测什么?
很多人刚接触启动优化时,会把:
Application.onCreate() 耗时
或者:
Activity.onCreate() 耗时
直接当成启动耗时。
这是不准确的。
因为真正的启动过程远不止这两个方法。
例如一次冷启动还包括:
-
创建 App 进程
-
ClassLoader 初始化
-
ContentProvider 初始化
-
Application 初始化
-
Activity 创建
-
Layout Inflate
-
Measure
-
Layout
-
Draw
-
RenderThread
-
首帧显示
因此:
单独测一个生命周期方法,不能代表完整启动时间。
真正评价启动体验,Android 中最重要的两个指标是:
TTID
和:
TTFD
二、什么是 TTID?
TTID 全称:
Time To Initial Display
可以理解成:
从用户启动 App,到第一帧真正显示出来需要多长时间。
例如:
用户点击 App:
0ms
↓
系统创建进程
↓
Application 初始化
↓
MainActivity 创建
↓
首页布局绘制
↓
第一帧显示:
340ms
那么这次:
TTID ≈ 340ms
TTID 关注的是:
用户什么时候第一次看到 App 的页面。
所以它非常适合衡量:
启动首屏速度。
三、TTID 不代表页面已经完全可用
这里有一个很容易误解的地方。
假设首页启动过程是这样的:
0ms
用户点击 App。
↓
300ms
Activity 第一帧出来。
页面上已经有:
Logo、标题、底部导航。
↓
800ms
数据库读取完成。
↓
1100ms
首页列表加载完成。
↓
1300ms
页面所有关键功能可以使用。
那么:
TTID 可能只有:
300ms
但用户真正可以正常使用页面,却需要:
1300ms
所以:
首帧快,不代表真正启动体验一定好。
于是就有了另外一个指标:
TTFD。
四、什么是 TTFD?
TTFD 全称:
Time To Full Display
可以理解成:
从启动 App,到页面真正完成关键内容加载的时间。
TTID 关注:
"页面出来了吗?"
TTFD 关注:
"页面真正准备好了吗?"
例如一个新闻 App。
首屏 300ms 出现:
-
Toolbar
-
Tab
-
白色列表区域
但是新闻数据 900ms 才显示。
那么:
TTID:
大约 300ms
TTFD:
可能接近 900ms
这两个指标关注的是不同阶段。
五、TTID 和 TTFD 怎么理解最简单?
可以简单记成:
TTID
用户什么时候看到东西。
TTFD
用户什么时候真正可以正常使用。
举个更加直观的例子。
一个外卖 App:
点击图标。
↓
300ms:
首页框架出来了。
↓
600ms:
定位完成。
↓
800ms:
商家列表出现。
↓
1000ms:
优惠、推荐等数据全部加载完成。
那么:
TTID 可能是 300ms。
而:
TTFD 可能接近 800~1000ms。
所以一个比较完整的启动优化,不能只盯着:
第一帧。
最终还要关注:
用户真正进入可用状态的时间。
六、为什么不能自己简单打时间戳?
早期我们很容易这么做:
在 Application 开始的位置记一个时间。
然后在 Activity.onResume() 再记一个时间。
两者相减:
这就是启动时间。
这种方式可以辅助分析某段业务代码,但作为完整启动性能指标并不可靠。
因为它没有包含完整的系统启动过程。
比如:
Application 开始计时之前,系统已经做过:
-
进程创建
-
Runtime 初始化
-
ClassLoader 初始化
-
一部分 Framework 工作
另外:
Activity.onResume() 被调用,也并不代表:
第一帧已经真正显示到屏幕。
所以自己打点更适合:
分析具体业务阶段耗时。
而完整启动性能测试,更适合使用系统提供的性能测试工具。
现在官方比较推荐的一套方案就是:
Macrobenchmark
七、Macrobenchmark 是什么?
Macrobenchmark 是 Android Jetpack 提供的一套:
应用级性能测试工具。
它和普通 Unit Test 不一样。
Macrobenchmark 可以直接针对一个真实 App 做性能测试,例如:
-
冷启动
-
温启动
-
热启动
-
页面滚动
-
页面跳转
-
Frame 性能
-
Baseline Profile 相关性能验证
对于启动优化来说,它最大的作用就是:
建立稳定、可重复的启动性能基线。
我们可以让它自动:
启动 App
↓
记录性能数据
↓
关闭 App
↓
重新启动
↓
重复多次
↓
输出最终结果。
相比自己拿秒表或者打印 Log,要可靠得多。
八、为什么 Benchmark 要单独建一个模块?
Macrobenchmark 通常不会直接写在业务 App 模块里面。
项目结构一般类似:
app
或者我们当前项目里的:
ReDexSample
负责真正运行的 App。
另外建立:
benchmark
模块。
benchmark 模块负责:
从外部控制目标 App,并测量它。
也就是说:
benchmark
↓
启动
↓
ReDexSample
↓
记录启动性能。
这种设计非常重要。
因为我们想测的是:
App 从外部真正启动时的完整性能。
而不是 App 自己启动之后再开始计时。
九、Macrobenchmark 启动测试的核心
一个典型的启动 Benchmark,本质上主要需要告诉系统三件事情:
第一:
测谁?
例如:
com.sample.redex
第二:
测什么?
例如:
StartupTimingMetric
第三:
怎么启动?
例如:
StartupMode.COLD
然后 Macrobenchmark 会多次执行:
停止 App
↓
冷启动 App
↓
记录启动耗时
↓
保存 Trace
↓
再次执行。
十、StartupMode.COLD 是什么?
Macrobenchmark 支持不同启动模式。
最重要的就是:
COLD
冷启动。
App 进程不存在。
需要重新:
-
创建进程
-
Application 初始化
-
Activity 创建
-
首帧绘制
这是启动优化最关注的模式。
WARM
温启动。
进程通常仍然存在,但是 Activity 需要重新创建或恢复相应状态。
HOT
热启动。
App 和 Activity 的大部分状态仍然存在。
主要是把页面重新带回前台。
在实际启动优化中:
通常优先分析 Cold Startup。
因为冷启动链路最长,也最容易暴露问题。
十一、StartupTimingMetric 测什么?
Macrobenchmark 做启动测试时,经常会使用:
StartupTimingMetric
它主要帮助我们获取启动相关的时间指标。
其中最常见的就是:
timeToInitialDisplayMs
也就是:
TTID。
如果应用正确上报"页面已经完全准备完成"的时机,还可以进一步观察 Full Display 相关指标。
所以我们后面看到:
timeToInitialDisplayMs
基本可以直接理解为:
从启动到首帧显示耗时。
十二、Iteration 是什么意思?
启动性能存在波动。
同一个 App 连续启动五次,不可能每一次都是:
340.2ms
可能是:
第一次:
352ms
第二次:
325ms
第三次:
387ms
第四次:
340ms
第五次:
329ms
原因很多,例如:
-
CPU 调度
-
系统后台任务
-
IO
-
温度
-
缓存状态
-
GC
-
设备负载
所以性能测试不能只跑:
一次。
Macrobenchmark 会通过:
Iterations
重复执行测试。
例如:
5 次。
然后输出:
-
min
-
median
-
max
等结果。
十三、为什么我更关注 median?
假设 5 次结果是:
325ms
330ms
340ms
350ms
387ms
其中:
最小值:
325ms
最大值:
387ms
中位数:
340ms
如果只看最小值:
可能过于乐观。
如果只看最大值:
又可能被某次系统抖动影响。
所以实际分析时,我通常更关注:
Median,中位数。
它更适合作为当前版本的性能基线。
当然,真正线上做性能监控时,还会进一步看:
P50、P90、P95 等分位数。
十四、一次真实的 Macrobenchmark 测试
我目前测试的项目是:
Chapter22-master
目标 App 模块:
ReDexSample
Benchmark 模块:
benchmark
测试设备:
小米 M2102K1AC
系统:
Android 14 / API 34
测试的是:
Cold Startup
运行:
5 次 Iteration
最终 Macrobenchmark 输出:
ExampleStartupBenchmark_startup
指标:
timeToInitialDisplayMs
结果:
-
min:324.7ms
-
median:340.2ms
-
max:387.0ms
因此当前可以先建立一个启动性能基线:
TTID median ≈ 340.2ms
这一步非常重要。
因为从现在开始,后面所有启动优化都必须和这个数字比较。
十五、340ms 到底算不算慢?
看到:
340ms
很多人第一反应就是:
这个数字到底好不好?
实际上不能简单脱离场景判断。
启动速度会受到很多因素影响:
-
设备性能
-
Android 版本
-
App 复杂程度
-
首页布局
-
SDK 数量
-
数据加载方式
-
是否使用 Profile
-
Release / Debug 构建方式
所以性能优化更重要的不是:
和别人比一个绝对数字。
而是:
建立自己的基线,然后持续对比。
例如现在:
优化前:
340.2ms
如果优化 Application 后:
315ms
再优化首页布局:
290ms
再使用 Baseline Profile:
260ms
那么我们就能非常清楚地知道:
每一次优化到底有没有效果。
十六、Macrobenchmark 最大的价值不是"测一个数字"
Macrobenchmark 真正有价值的地方还有一个:
它会生成 System Trace。
例如我们跑 5 次测试以后,会看到:
Iteration 0
Iteration 1
Iteration 2
Iteration 3
Iteration 4
每一次启动都对应一条 Trace。
这些 Trace 可以使用:
Perfetto / System Trace
打开。
然后我们就可以继续往下分析:
340ms 到底花在哪里?
例如:
是:
bindApplication
比较慢?
还是:
Application.onCreate
比较慢?
还是:
Activity.onCreate
比较慢?
还是:
traversal
比较慢?
又或者:
主线程大量:
Runnable
一直抢不到 CPU?
这就把启动优化从:
"感觉慢"
变成了:
"有数据、有 Trace、有证据。"
十七、Macrobenchmark 和 Perfetto 的关系
这两个工具千万不要混淆。
可以简单记成:
Macrobenchmark
回答:
有多慢?
例如:
TTID:
340.2ms
Perfetto
回答:
为什么慢?
例如:
Application:
60ms
Activity:
90ms
Layout:
40ms
IO:
30ms
线程等待:
20ms
......
所以完整流程应该是:
Macrobenchmark
↓
测出:
340ms
↓
Perfetto
↓
分析:
340ms 花在哪里。
这才是真正进入启动优化。
十八、为什么性能测试最好使用真机?
启动性能和:
-
CPU
-
内存
-
IO
-
调度
-
系统版本
都有直接关系。
模拟器的运行环境和真实手机差别非常大。
所以性能优化中一个很重要的原则就是:
尽量使用真机测试。
并且最好固定:
-
同一台设备
-
同一个系统版本
-
同一种构建类型
-
相同测试场景
否则前后结果可能不具备可比性。
例如:
优化前用旗舰机测试。
优化后换了一台老设备。
那两个数字比较基本没有意义。
十九、启动性能测试还要注意环境稳定
即使使用同一台设备,也需要尽量减少外部干扰。
例如测试过程中:
后台正在下载文件。
或者:
手机正在发热降频。
或者:
系统正在执行大量后台任务。
都会影响结果。
因此一次性能优化最好不要只看:
某一次测试结果。
而是通过多次测试观察:
整体趋势。
例如:
优化前:
Median = 340ms
连续测试基本在:
330~360ms
优化后:
Median = 285ms
连续测试基本在:
275~300ms
这种下降才更有说服力。
二十、不要为了数字而优化
还有一点非常重要。
启动优化不是为了:
把 Benchmark 数字做到最低。
最终还是要回到:
用户体验。
例如一种"优化"方式:
App 300ms 就显示一个空白首页。
所有真正内容:
2 秒以后才加载。
那么 TTID 的确非常漂亮。
但用户体验可能反而更差。
所以真正完整的启动优化,最终应该同时关注:
首帧速度
和:
页面真正可用速度。
也就是:
TTID
TTFD
而不是单纯追求一个更小的首帧数字。
二十一、建立启动优化基线
到这里,我们现在已经有了第一组非常重要的数据:
当前版本:
Cold Startup
设备:
小米 M2102K1AC / Android 14
测试工具:
Macrobenchmark
Iterations:
5
TTID:
-
min:324.7ms
-
median:340.2ms
-
max:387.0ms
因此:
340.2ms 就是我们当前启动优化实验的基准线。
以后每做一次优化:
都应该重新跑 Macrobenchmark。
例如:
业务初始化优化:
↓
重新测。
Baseline Profile:
↓
重新测。
Startup Profile:
↓
重新测。
DEX 类重排:
↓
重新测。
最终用数据判断:
到底有没有真正改善启动性能。
二十二、小结
这一篇主要解决了一个问题:
Android 启动速度到底应该怎么测?
我们先认识了两个核心指标:
TTID
表示:
从启动到第一帧显示。
TTFD
表示:
从启动到页面真正完成关键内容加载。
然后使用:
Macrobenchmark
建立稳定的启动性能基线。
当前实测结果:
TTID median ≈ 340.2ms
但这时候我们仍然只知道:
App 启动需要大约 340ms。
还不知道:
这 340ms 到底花在哪里。
因此下一篇就正式进入启动优化最关键的一步:
Android 启动优化(三):Perfetto 实战------340ms 启动时间到底花在哪里?
下一篇我们会开始真正看:
-
Main Thread
-
bindApplication
-
Application
-
Activity
-
Running
-
Runnable
-
Waiting
-
Wall Duration
-
CPU Duration
-
IO
-
Binder
-
GC
-
Choreographer
-
traversal
从:
"测出来慢"
正式进入:
"找到为什么慢"。