冷启动 Trace 生成实战:三种主流方式

我记得很多刚接触冷启动优化的兄弟,上来就问:“Trace 到底怎么抓?” 这问题看着简单,但其实坑不少。今天我把自己压箱底的三个招儿全摊出来:adb 命令、代码自动埋点、Perfetto 配置。你想想看,三种方式各有脾气,用对了能救命,用错了……嗯,我当年在线上版本里埋过一个坑,后面细说。

一、adb 命令快速抓取

这是最直接的方式,适合开发阶段快速验证。核心命令就一条,但参数搭配有讲究。

基础命令

adb shell am start -W -P /data/local/tmp/trace.perfetto \
  -S com.example.app/.MainActivity

解释一下:-W 是等待启动完成,-P 指定 Perfetto 输出路径,-S 会先杀掉进程再冷启动。我习惯把 trace 文件存到 /data/local/tmp/,权限问题少。

我的小习惯:抓完立刻 pull 到本地,别等设备重启。有次我忘了,设备一重启文件被系统清理,白忙活半小时。

进阶:自定义时长与缓冲区

adb shell perfetto \
  -c /data/local/tmp/config.pbtx \
  -o /data/local/tmp/trace.perfetto \
  --txt

实际项目里,你很少用 default 配置。比如我调微信冷启动时,默认 4MB 缓冲区根本不够,主线程 Binder 调用多到爆。我后来改成 32MB 加 30 秒时长,才抓到完整的 IO 链。

参数作用推荐值
--buffer-size缓冲区大小16~32MB
--duration抓取时长30~60秒(冷启动看前10秒)
--atrace-categories系统分类sched, freq, idle, am, wm

⚠️ 注意:--atrace 分类不要全开,否则数据量大到 Perfetto 直接 OOM。我见过有人开了 20 多个 atrace 分类,手机直接重启——这就是典型的“过度抓取综合症”。

二、代码自动埋点:精准控制每一帧

adb 方式有一个硬伤:抓不住 Application 初始化之前的事件。说白了,你 App 的 attachBaseContext、ContentProvider 初始化这些关键路径,adb am start 根本覆盖不到。怎么办?上代码埋点。

构建 Perfetto TraceConfig

我建议你在 Application.attachBaseContext 里就启动 Trace,因为那时候连四大组件都还没创建。

class MyApp : Application() {
  override fun attachBaseContext(base: Context) {
    super.attachBaseContext(base)
    // 尽早启动 trace
    PerfettoConfigBuilder().build().also { config ->
      PerfettoState.initialize(config)
      PerfettoState.startTracing()
    }
  }
}

代码 vs adb 的核心区别:代码埋点能用 Trace.beginSection 加自定义标签。我常用标签名带上业务链路 ID,方便回溯。

Trace.beginSection("ColdStart:InitOKHttp")
// 初始化 OKHttp
Trace.endSection()

重点:自定义标签命名规范。我曾经踩过坑——标签名太长,超过 127 字符会被截断,Perfetto UI 里变成乱码。现在我都控制在 60 字符以内,格式统一 模块:操作名。

何时停止 Trace

冷启动 Trace 应该在首帧绘制完成后再持续 2~3 秒,确保后台线程也记录完整。我用一个 FrameCallback 监听首帧,然后延迟 3 秒自动停止并 dump 文件。

Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
  override fun doFrame(frameTimeNanos: Long) {
    if (isFirstFrame) {
      handler.postDelayed({
        PerfettoState.stopTracing("/sdcard/cold_trace.perfetto")
      }, 3000)
    }
  }
})

为什么延迟 3 秒?我踩过坑:首帧刚出来就停 Trace,结果 IO 线程正在写 SharedPreferences,后面崩溃的堆栈全丢了。留点余量,数据才完整。

三、Perfetto 配置文件编写

无论 adb 还是代码埋点,最终都要靠 .pbtx 配置文件。这个文件决定了你能看到哪些数据。很多人上来就复制网上的配置,结果关键数据没抓到,还得重新跑一遍——我当年就干过这种蠢事。

标准配置模板

写一个 cold_start.pbtx,专门用来抓冷启动。

buffers: {
  size_kb: 40960
  fill_policy: RING_BUFFER
}
data_sources: {
  config {
    name: "linux.process_stats"
    target_buffer: 0
  }
}
data_sources: {
  config {
    name: "android.surfaceflinger.frametimeline"
  }
}
data_sources: {
  config {
    name: "android.app_startup"
  }
}
data_sources: {
  config {
    name: "track_event"
    target_buffer: 0
  }
}
duration_ms: 15000
data_source抓什么冷启动必开?
linux.process_statsCPU 调度、线程优先级✅ 必开
android.surfaceflinger.frametimeline帧渲染、丢帧✅ 必开
android.app_startupAMS 视角的启动阶段✅ 必开,否则你看不到冷启动分阶段
track_event自定义标签(代码埋点)✅ 必开,否则你的 beginSection 无效

💡 我团队里新来的同事总喜欢把 file 相关的 data_source 全开,比如 android.sysfs_read,结果 buffer 被日志刷爆。冷启动场景下,你只需要关注 android.app_startup 和 track_event,其他按需加。

实用技巧:区分冷启动 vs 温启动

用 Perfetto 看冷启动,最关键的一个信号是 ProcessRecord 中的 PROCESS_START 事件。我写配置文件时会加一个触发条件:当 android.app_startup 检测到包名为 com.example.app 的进程被创建时,自动开始抓取——这样避免抓一堆无用的系统启动。

trigger: {
  name: "startup_trigger"
  manual: false
  config: {
    trigger_name: "android.app_startup"
    package_name: "com.example.app"
  }
}

嗯,这里要注意:trigger 配置只在 Android 12+ 才能用。如果你们还在维护 Android 11 以下版本,老老实实手动 am start。

三种方式对比:一张图说清楚

下面这张图是我自己画的,把三种生成 Trace 的流程和适用场景串起来了。你一看就明白什么时候该用哪个。

冷启动Trace生成方式对比流程 生成冷启动 Trace adb shell am start -P 适合快速验证/开发 代码自动埋点 精准控制起始点/自定义标签 Perfetto配置文件 自定义数据源/触发器 缺点:抓不住App初始化 数据源局限 优点:可捕获attachBaseContext 搭配自定义标签追踪业务 需要模板 & 版本兼容 Android 12+ 触发器好用 推荐组合 开发用adb + 线上用代码埋点 + Perfetto统一配置

看明白了吧?三种方式不是互斥的。我在实际项目中就是这么干的:开发阶段用 adb 快速迭代,看整体启动链路;集成测试和线上灰度用代码埋点,抓特定模块的耗时;Perfetto 配置文件统一管理,保证所有环境的数据源一致,方便对比。

⚠️ 最后提醒一句:不管用哪种方式,Trace 文件最好加时间戳命名,比如 cold_trace_20240315_1400.perfetto。我有一次没加时间戳,第二天被测试覆盖了文件,重新抓了一天,还错过了修复窗口。血的教训。