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,示例数据,用于说明计算方法):

打点含义时间戳
P0am_proc_start 事件10240
P1attachBaseContext10312
P2appOnCreate_start10315
P3appOnCreate_end10438
P4activityOnCreate_start10441
P5activityOnCreate_end10502
P6activityOnResume10510
P7firstFrameDrawn10568

按阶段相减:进程创建 = P1−P0 = 72 ms;Application 初始化 = P3−P1 = 126 ms;Activity 创建 = P6−P3 = 72 ms;首帧渲染 = P7−P6 = 58 ms。总冷启动 = P7−P0 = 328 ms。

单次采集受系统调度抖动影响很大,基线必须取多次(建议 ≥10 次)的中位数或 P90,并固定设备型号、系统版本、电量与后台负载,否则数据不可比。

四阶段链路与耗时归属

冷启动四阶段耗时链路(示例:总 328 ms) P0 进程创建起点 P7 首帧上屏 进程创建 72 ms Application 初始化 126 ms Activity 创建 72 ms 首帧渲染 58 ms P0 → P1 P1 → P3 P3 → P6 P6 → P7 阶段归属与优化方向 进程创建:受系统调度与后台进程数影响,应用侧可优化空间有限 Application 初始化:SDK 注册、全局配置,是应用侧最可控的优化点 Activity 创建:布局层级、setContentView 复杂度、视图树构建 首帧渲染:测量/布局/绘制耗时、RenderThread 提交、合成上屏

基线数据记录与对比

把每次采集的四阶段耗时写入表格,取中位数作为基线。后续任何优化都必须与同一基线对比,才能判断是否真正有效。

采集轮次进程创建ApplicationActivity首帧渲染总计
1721267258328
2681317061330
3751247456329
中位数721267258328
基线建立后,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 确保进程完全退出。