使用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 卡顿分析 或 真正首帧耗时统计

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器