TraceSection 和 Perfetto,我是怎么分析卡顿的

TraceSection 和 Perfetto,我是怎么分析卡顿的

TraceSection 和 Perfetto,我是怎么分析卡顿的


「TraceSection 和 Perfetto,我是怎么分析卡顿的」


去年维护的一个社交 App,用户反馈信息流滑动时有“顿一下”的感觉。复现环境是 Pixel 6 Android 14,帧率统计里看平均 FPS 接近 60,但滑动过程中肉眼可见地抽一下。我先用传统 Systrace 抓了一下,RecyclerView 的 onBindViewHolder 里那段逻辑偶尔跳到 10ms,可看代码只是简单的数据绑定,Systrace 视图里这段区域被标成绿色,没什么异常。切换到 Perfetto,把采样率调高,才看到在卡顿发生的前一帧,有一个跨进程的 ContentProvider 查询卡住了 UI 线程的 binder 线程池。这个发现让我意识到,分析工具的分辨率和视角差异,直接决定了你能不能看到真相。


Systrace 的盲区与 Perfetto 的增量信息


Systrace 基于 ftrace,采样间隔固定,默认按屏幕刷新率或更低频率打点,对短于采样间隔的突发卡顿不敏感。Android 10(API 29)开始,Perfetto 成为系统默认的 trace 守护进程,Systrace 脚本在 Platform Tools 里已经被标记为 legacy。Perfetto 使用 protobuf 编码的 trace 文件,用 ui.perfetto.dev 打开,文件体积更小,解析更快。关键是 Perfetto 支持长时间录制和更高频率的 ftrace 采样,还能把 atrace、ftrace、heapprofd、power rails 多个数据源合并到同一个时间轴上。


实际差异在分析卡顿的时候非常明显。Systrace 的 HTML 报告里,如果你的卡顿发生在两采样点之间,它可能被相邻的正常帧平均掉,看起来只是一条略宽的绿色条带。Perfetto 的纳秒级时间戳和更密集的 sched_switch 事件,能让你看到线程被抢占了几个毫秒、在哪个 CPU 核心上被什么进程抢走的。这种细节在分析“为什么这一帧突然多了 8ms”的时候往往是决定性的。


android.os.Trace.beginSection 的隐藏开销


很多人直接在代码里写:


Trace.beginSection("loadData " + userId);

这里有个隐患:即使 atrace 没有开启,Trace.isEnabled() 返回 false,Java 层的字符串拼接仍然会执行。从 ART 的方法调用约定来看,方法参数总是先求值再入栈,所以 "loadData " + userId 这个表达式在每次调用时都会走一次 StringBuilder。在 RecyclerView 的 onBind 里高频调用,本身就贡献掉帧。


正确的做法应该是:


if (Trace.isEnabled()) {
    Trace.beginSection("loadData");
}

Trace.isEnabled() 在 API 23(Android 6.0)才加入。对于兼容旧版本,可以用 AndroidX 的 TraceCompat,但 TraceCompat.isEnabled() 内部实现需要反射获取 TRACE_TAG_APP 的 mask,也有开销。实际测下来,在小米 13 上,isEnabled() 调用大约 40-80ns,完全可以接受;而字符串拼接的代价在几百 ns 到几 us,在 16ms 的帧时间里不该被忽略。


API 29 之后,Trace.beginSection 内部通过 atrace 写到了 ftrace ring buffer。如果 ring buffer 满,写操作会休眠或丢事件。这个在 Perfetto 里能看到 trace buffer overrun 的标记。我曾在一次长链路测试里连续埋了上百个 trace point,最后发现后半段 trace 完全空白,就是因为默认 8MB 的 buffer 被冲掉了。


还有一个坑是 section 名的长度。旧版本 Android(API 28 及以下)里 atrace 的 `MAX_TRACE

Compose 性能分析:Layout Inspector 之外还有什么 2026-08-26
Compose Multiplatform 真的能用吗?踩坑记录 2026-08-31

评论区