1. 冷启动耗时基线采集与分阶段拆解
为什么冷启动必须分阶段测量
冷启动是进程从零开始到首帧上屏的完整链路。只记录一个总耗时,无法判断瓶颈在系统调度、应用初始化还是布局渲染。把冷启动拆成四个可独立观测的阶段,才能让优化有明确的靶点:
- 进程创建:AMS 请求 Zygote fork 出新进程,到进程入口可执行。此阶段应用代码尚未运行,主要受系统负载与进程数影响。
- Application 初始化:
Application.attachBaseContext()→onCreate()执行完毕。第三方 SDK、全局配置、埋点、崩溃监控通常在这里注册。 - Activity 创建:
Activity.onCreate()→onResume()完成,包含 setContentView 与视图树构建。 - 首帧渲染:View 测量/布局/绘制 → RenderThread 提交 → SurfaceFlinger 合成上屏,即用户真正看到画面的时刻。
采集方案:用 Logcat 时间戳做阶段打点
最稳定的真机基线采集方式是在关键节点打印带统一 TAG 的日志,再用脚本解析时间戳。相比 adb shell am start -W,它能给出阶段边界,而不只是一个 TotalTime。
在 Application 与首个 Activity 中插入打点(示例代码,可直接放入工程):
// 统一 TAG,便于 logcat 过滤
private const val TAG = "StartupTrace"
class MyApp : Application() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
Log.i(TAG, "P1_attachBaseContext")
}
override fun onCreate() {
super.onCreate()
Log.i(TAG, "P2_appOnCreate_start")
// ... 此处为真实的 SDK 初始化逻辑
Log.i(TAG, "P3_appOnCreate_end")
}
}
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(TAG, "P4_activityOnCreate_start")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.i(TAG, "P5_activityOnCreate_end")
}
override fun onResume() {
super.onResume()
Log.i(TAG, "P6_activityOnResume")
// 首帧上屏回调,API 16+ 可用
window.decorView.viewTreeObserver
.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
window.decorView.viewTreeObserver.removeOnPreDrawListener(this)
Log.i(TAG, "P7_firstFrameDrawn")
return true
}
})
}
}
进程创建阶段的起点无法由应用代码打点,需借助
adb shell am start -W 输出的 TotalTime 与 WaitTime,或使用 logcat -b events 中的 am_proc_start 事件时间戳作为 P0 起点。四阶段耗时计算与基线表
假设一次真机采集得到如下时间戳(单位 ms,示例数据,用于说明计算方法):
| 打点 | 含义 | 时间戳 |
|---|---|---|
| P0 | am_proc_start 事件 | 10240 |
| P1 | attachBaseContext | 10312 |
| P2 | appOnCreate_start | 10315 |
| P3 | appOnCreate_end | 10438 |
| P4 | activityOnCreate_start | 10441 |
| P5 | activityOnCreate_end | 10502 |
| P6 | activityOnResume | 10510 |
| P7 | firstFrameDrawn | 10568 |
按阶段相减:进程创建 = P1−P0 = 72 ms;Application 初始化 = P3−P1 = 126 ms;Activity 创建 = P6−P3 = 72 ms;首帧渲染 = P7−P6 = 58 ms。总冷启动 = P7−P0 = 328 ms。
单次采集受系统调度抖动影响很大,基线必须取多次(建议 ≥10 次)的中位数或 P90,并固定设备型号、系统版本、电量与后台负载,否则数据不可比。
四阶段链路与耗时归属
基线数据记录与对比
把每次采集的四阶段耗时写入表格,取中位数作为基线。后续任何优化都必须与同一基线对比,才能判断是否真正有效。
| 采集轮次 | 进程创建 | Application | Activity | 首帧渲染 | 总计 |
|---|---|---|---|---|---|
| 1 | 72 | 126 | 72 | 58 | 328 |
| 2 | 68 | 131 | 70 | 61 | 330 |
| 3 | 75 | 124 | 74 | 56 | 329 |
| 中位数 | 72 | 126 | 72 | 58 | 328 |
基线建立后,Application 初始化占 126 ms(约 38%),是当前最大的可优化阶段。后续优化应优先从这里入手,而不是盲目调整布局。
采集脚本与验证
用以下命令过滤日志并提取时间戳,可快速得到阶段耗时:
adb logcat -c
adb shell am start -W -n com.example.app/.MainActivity
adb logcat -d -s StartupTrace:I | grep -E "P[0-9]_"
验证基线是否可信的三个条件:同一设备连续 10 次采集,总耗时波动不超过 ±10%;四阶段之和与 am start -W 的 TotalTime 偏差在 15 ms 以内;每次采集前用 adb shell am force-stop 确保进程完全退出。