Choreographer 的帧回调,自己监听能做什么

Choreographer 的帧回调,自己监听能做什么

Choreographer 的帧回调,自己监听能做什么


「Choreographer 的帧回调,自己监听能做什么」


去年把项目里的性能监控 SDK 从基于 Handler 的埋点方案切到 Choreographer 时,我满以为这只是一个简单的 API 替换。毕竟 Choreographer 从 API 16 就已经存在,postFrameCallback 的签名十几年没变过,能有什么坑?结果在 Android 14 的 Pixel 7 Pro 上跑第一轮集成测试时,帧率曲线就给了我当头一棒:静止页面显示 30fps,滑动时瞬间跳到 120fps,手指松开后又回落到 30fps,而我的旧代码还在用 16.6ms 的固定阈值判断丢帧,整个监控看板几乎变成了随机数生成器。这才发现,我们对 Choreographer 帧回调的使用方式,还停留在 60Hz 屏幕时代的惯性里。


屏幕刷新率变了,你的阈值还在用 16ms 吗


早期的 Android 设备清一色 60Hz,两帧之间的理论间隔就是 16.67ms。所以很多老项目里自研的帧率监控逻辑都硬编码了这个数字:判断丢帧的算法简单粗暴,当前帧时间与上一帧相差超过 16.67ms,就算掉了一帧;超过 33.33ms,算掉了两帧。这个逻辑在 2019 年之前基本够用,但现在的中高端机型普遍是 90Hz 甚至 120Hz,入门机也可能因为省电策略动态降频到 30Hz。


问题的核心在于,Choreographer 的 doFrame(long frameTimeNanos) 回调并不保证以固定间隔触发,它的节奏完全由屏幕的 VSYNC 信号驱动。如果你的应用运行在 120Hz 模式下,正常的帧间隔应该是 8.33ms。此时你的代码如果还按 16.67ms 去判定,会把大量正常帧误判为掉帧。反过来,如果系统处于 30Hz 的省电模式,正常间隔是 33.33ms,你的监控又会把系统故意的降帧当成严重卡顿来报警。


正确的做法不是写死任何阈值,而是在运行时获取当前 Display 的刷新率。从 API 17 开始可以用 Display.getRefreshRate(),到 API 23 之后更推荐使用 Display.Mode.getRefreshRate(),后者在支持多显示模式的设备上更准确。拿到刷新率后,计算出当前的理论帧间隔 refreshPeriodNanos = 1_000_000_000L / refreshRate,再把实际间隔和它比较。即便如此也不是万无一失,因为 Android 14 引入的自适应刷新率策略会让刷新率在几毫秒之内发生跳变,你刚查到的 120Hz 可能下一个 VSYNC 周期就变成了 60Hz,这是后话。


postFrameCallback 是一次性的,这不是循环事件


这是我踩过最尴尬的坑,也是 GitHub 上很多开源监控库至今没修好的问题。Choreographer 的 postFrameCallback(FrameCallback callback) 是单次触发的,它不像 View.postInvalidateOnAnimation() 那样会自动循环。很多示例代码,甚至早期官方文档角落里的一些示例,都暗示只要在 Activity onResume 里 post 一次,就能持续收到每一帧的回调。真相是,doFrame 执行完这一次后,如果你不手动再次 postFrameCallback,监听就自动停止了。


如果你想做持续的帧率统计,必须在 doFrame 方法内部再次把自己 post 出去。这带来了两个直接的副作用。第一,如果上一帧发生了严重卡顿,doFrame 迟迟不被调用,你的监控逻辑也会跟着暂停,直到卡顿结束才会恢复。这其实是好事,说明监控本身不会加剧问题,但它也意味着你拿不到卡顿期间的详细帧分布。第二,也是更要命的,如果你在 Activity 退出时忘了调用 removeFrameCallback,而你的 FrameCallback 内部又持有了 Activity 的隐式或显式引用,内存泄漏是板上钉钉的。因为 Choreographer 是跟着 Thread 的 Looper 生命周期走的,比 Activity 活得更久。


我在实际项目里的做法是维护一个 AtomicBoolean 的开关状态,在 doFrame 开头就检查是否已停用,如果是,直接 return 不再 re-post。这样即使 removeFrameCallback 因为某些异步竞态没成功,也不会形成死循环。


private final Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        if (!isMonitoring) return;
        // ... 计算逻辑 ...
        Choreographer.getInstance().postFrameCallback(this);
    }
};

frameTimeNanos 是 VSYNC 时间,不是 CPU 执行时间


doFrame 回调的参数名 frameTimeNanos 很有误导性。我第一次用的时候理所当然地把它理解为"这一帧开始执行时的 CPU 时间戳",然后拿它跟 System.nanoTime() 对比,想算一下 Choreographer 回调自身的延迟。这个对比毫无意义。


根据 AOSP 源码中 Choreographer 类的注释和实现,frameTimeNanos 代表的是本次 VSYNC 信号到达应用进程的时间戳。系统底层的 SurfaceFlinger 在每个 VSYNC 边界唤醒等待的进程,Choreographer 把 INPUT、ANIMATION、TRAVERSAL 三类任务按顺序丢进消息队列,等到真正执行到你在 postFrameCallback 里注册的这个 CALLBACK_COMMIT 优先级任务时,CPU 时间可能已经过去了若干毫秒。尤其是在主线程被消息队列阻塞时,你收到的 frameTimeNanos 和当前 `System.nano

Sentry 的崩溃监控集成,比 Firebase 差在哪 2026-07-29
Android 14 的部分照片访问权限,用户体验真的变好了吗 2026-07-29

评论区