冷启动 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_stats | CPU 调度、线程优先级 | ✅ 必开 |
| android.surfaceflinger.frametimeline | 帧渲染、丢帧 | ✅ 必开 |
| android.app_startup | AMS 视角的启动阶段 | ✅ 必开,否则你看不到冷启动分阶段 |
| 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 的流程和适用场景串起来了。你一看就明白什么时候该用哪个。
看明白了吧?三种方式不是互斥的。我在实际项目中就是这么干的:开发阶段用 adb 快速迭代,看整体启动链路;集成测试和线上灰度用代码埋点,抓特定模块的耗时;Perfetto 配置文件统一管理,保证所有环境的数据源一致,方便对比。
⚠️ 最后提醒一句:不管用哪种方式,Trace 文件最好加时间戳命名,比如 cold_trace_20240315_1400.perfetto。我有一次没加时间戳,第二天被测试覆盖了文件,重新抓了一天,还错过了修复窗口。血的教训。