列表滑动掉帧,是 App 的锅还是 SurfaceFlinger 的锅?用 FrameTimeline 把 16ms 流水线拆开
一个电商列表页在中端机上滑动时偶发掉帧,开发侧用 Choreographer 的 Skipped frames 日志看到丢帧,就认定是“主线程做了耗时操作”。但把主线程的 Trace 拉出来看,doFrame 里并没有明显的长耗时。问题卡在这里:知道掉帧了,但不知道这一帧的 deadline 是在哪一级流水线上被吃掉的。Android 12 引入的 FrameTimeline 解决的正是这个问题——它把一帧从 VSync 到屏幕亮像素的整条链路拆开,直接告诉你 jank 归因在应用侧还是系统侧。
一、 16ms 不是“主线程的 16ms”,而是整条流水线的 deadline
在 60Hz 屏幕上,一帧的总预算是 16.6ms,但这条流水线是分级的:VSync 触发 → Choreographer 的 INPUT / ANIMATION / TRAVERSAL → RenderThread 的 SYNC / DRAW → SurfaceFlinger 合成 → 显示。每一级都有自己的时间窗口,如果主线程的 INPUT + ANIMATION + TRAVERSAL 吃掉了 12ms,留给 RenderThread 的只剩 4ms,GPU 负载稍高这一帧就没了。只盯着主线程看,很容易漏掉 RenderThread 阻塞或 SurfaceFlinger 合成延迟造成的掉帧。
二、 FrameTimeline 的归因:AppDeadlineMissed vs 系统侧
Perfetto 的 FrameTimeline 会给每一帧标记 jankType。关键分类是:AppDeadlineMissed(应用自身没在截止时间前完成)、SurfaceFlinger / SF scheduling 等系统侧原因,以及 PredictionError 等。SurfaceFrame::isSelfJanky() 只把 AppDeadlineMissed、Unknown、AppResyncedJitter 归入该 layer 自身造成的 jank,其余归系统侧。如果 jankType 是 AppDeadlineMissed,瓶颈才在应用进程;如果是系统侧归因,继续在主线程里找耗时操作就是方向性错误。
我们遇到的案例最终归因是 AppDeadlineMissed,但耗时并不在 doFrame 的同步代码里,而是在 RenderThread 的 SYNC 阶段被一个自定义 View 的 onDraw 中的对象分配拖慢——主线程的 TRAVERSAL 只负责记录绘制指令,真正的耗时发生在渲染线程回放 DisplayList 的时候。FrameTimeline 把 RenderThread 的时间线拉出来之后,这个阻塞点才暴露出来。
三、 排查顺序:先归因,再下钻
基于这次经验,掉帧排查的顺序应该固定为:先用 FrameTimeline 看 jankType 的分布,确认是应用侧还是系统侧占主导;如果是 AppDeadlineMissed,再按阶段拆——看 HANDLE_INPUT_START 到 PERFORM_TRAVERSALS_START 之间是否有耗时,DRAW_START 到 ISSUE_DRAW_COMMANDS_START 之间 RenderThread 是否被阻塞,以及 GPU 侧是否有异常负载。线下可以用 Perfetto 的 FrameTimeline 轨道直接看,线上则可以通过 FrameMetrics / JankStats 采集 jankType 做聚合统计,先定位问题集中在哪一类归因上,再决定是改业务代码、优化自定义 View 的绘制,还是和系统侧的问题做兼容。
四、 总结
列表掉帧的排查,最怕的是“看到丢帧日志就开始猜主线程”。FrameTimeline 的归因先回答“谁的锅”,阶段时间戳再回答“哪一段慢”。RenderThread 的 SYNC/DRAW 阶段、SurfaceFlinger 的合成调度,这些都是主线程 Trace 看不到但真实吃掉 deadline 的地方。把归因做在前面,优化才不会打在棉花上。
- 点赞
- 收藏
- 关注作者
评论(0)