使用postFrameCallback检测首帧耗时
Choreographer.postFrameCallback {} 并不一定意味着“当前这一帧已绘制完成”,它确实每 16.6ms(基于屏幕刷新率)执行一次,且可能是下一帧的开始而不是结束。
postFrameCallback 执行的时机可能早于真正的绘制完成(SurfaceFlinger 合成、提交屏幕),那么它真的能代表“用户看到首帧”的准确时机吗?
我们来拆解这个问题
1.Choreographer.postFrameCallback {}是在主线程“下一帧绘制开始前”执行的
确切说:
- 它是在 Vsync 信号到达时 执行的;
- 这时候主线程还没完成 measure/layout/draw;
- 所以你在 postFrameCallback 中打的时间点,其实是在那一帧准备开始绘制前的一瞬间。
这意味着:并不是“绘制完成”的时间点,只是“即将绘制”而已。
那为什么它还能用于“首帧耗时”统计?
实际上,它统计的是:
从 app 启动开始(Application.attachBaseContext)到“首个 Activity 真正参与一次完整的 UI 绘制前”的耗时
这个时间点虽然 略早于真正显示到屏幕,但:
- 已经完成了 Activity 初始化、布局加载、View 创建等核心逻辑;
- 系统正在准备进行真正意义的“第一帧绘制”;
- 是大多数用户感知到的界面展现“前的最后阶段”;
- 与 “用户实际看到的时间” 的误差极小(通常在几 ms 以内,视布局复杂度、设备刷新率而定);
所以它被广泛用于:
- Google 自身 reportFullyDrawn() 推荐的调用点;
- Meta、字节、腾讯等使用的 LaunchTime 埋点实践;
- 实际分析中,足够接近可视帧的结束时刻,统计首帧耗时具备可操作性。
但是它不是完全精准的“首帧渲染完成”时间
更加准确的方式(仅限调试)
如果你追求系统级精度:
方法 | 描述 |
---|---|
✅ 使用 FrameMetricsListener(API 24+) | 可以捕捉每一帧的绘制耗时,包括提交合成时机 |
✅ 使用 Perfetto、Systrace | 可以看到渲染管线、SurfaceFlinger 合成 |
✅ 使用 reportFullyDrawn() | 可追踪 app 首次渲染完整帧的时机 |
🚫 postFrameCallback | 接近、可接受,但非精确(未触及屏幕合成层) |
总结
方法 | 是否精准 | 是否推荐 |
---|---|---|
postFrameCallback | ❌ 不是“绘制完成”,但足够接近 | ✅ 推荐用于实际业务埋点 |
FrameMetricsListener | ✅ 更准确 | ✅ 用于分析优化 |
Perfetto/Systrace | ✅ 系统级最准确 | ✅ 推荐用于 Debug 阶段分析 |
reportFullyDrawn() | ✅ 官方建议的标志点 | ✅ 可结合使用 |
建议的改进方案(进阶)
如果你真的想更接近绘制完成:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
window.windowMetrics.let {
window.addOnFrameMetricsAvailableListener({ _, metrics, _ ->
val totalDurationNs = metrics.getMetric(FrameMetrics.TOTAL_DURATION)
Log.d("Frame", "Frame render took ${totalDurationNs / 1_000_000}ms")
}, Handler(Looper.getMainLooper()))
}
}
上面这种方式可以捕捉每一帧真实渲染耗时,更适合做 UI 卡顿分析 或 真正首帧耗时统计。