2. Choreographer 的帧率机制:每帧的调度与绘制

2.1 它到底在做什么?

Choreographer 翻译过来叫“编舞者”。名字挺文艺,干的却是硬核的活——协调输入、动画、绘制这三件事,让它们在一个垂直同步(VSync)周期里按顺序跑完。

我个人习惯把它理解成“Android 的节拍器”。你想想看,如果每个线程自己想什么时候刷新就什么时候刷新,画面肯定会撕裂或者跳帧。Choreographer 就是那个喊“1、2、3、开始”的人。

核心职责:

  • 接收底层硬件的 VSync 信号
  • 管理三种 callback 队列(输入、动画、绘制)
  • 按固定顺序触发回调,保证一帧内的所有工作在下一帧到来前完成

2.2 一帧到底怎么走?

我来画个流程:VSync 信号一到,Choreographer 就依次调用三个阶段的 callback:

  1. INPUT(输入处理)——处理触摸事件、按键事件
  2. ANIMATION(动画计算)——执行所有活跃的动画帧,更新属性值
  3. TRAVERSAL(遍历绘制)——触发 View 的 measure、layout、draw

我的经验是,大部分丢帧都出在第三阶段——特别是列表滑动时,如果 View 层级太深,一次遍历可能就超了 16ms。

一点小技巧:你可以在 logcat 里搜 Choreographer 关键字,看到类似 Skipped 47 frames! The application may be doing too much work on its main thread. 的警告。这个 47 就是掉帧数,数值越大问题越严重。

2.3 FrameCallback 与 vsync 监听

Choreographer 对外提供了两个主要入口:postFrameCallback 和 postFrameCallbackDelayed。前者就是下一帧开始前会调你的回调,后者可以延迟。

// 简单示例:自定义帧监听
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        // frameTimeNanos 是这一帧开始的时间(纳秒)
        long startNs = System.nanoTime();
        long durationMs = (startNs - frameTimeNanos) / 1_000_000;
        if (durationMs > 16) {
            Log.w("FrameMonitor", "掉帧 " + (durationMs - 16) + " ms");
        }
        // 持续监听下一帧
        Choreographer.getInstance().postFrameCallback(this);
    }
});

曾经我在一个短视频项目里就用这个接口做了帧率监控。当时线上反馈滑动卡顿,原生帧率工具又拿不到用户现场的日志。加了这个回调后,发现掉帧集中在某个自定义 ImageView 的 onDraw 里——那个控件每次都要 decode 本地图片。改成预加载后,帧率就稳了。

2.4 掉帧根本原因——超时

Choreographer 的调度模型很简单:VSync 到来 -> 执行三个阶段的 callback -> 等待下一个 VSync。

如果一个阶段的 callback 执行时间超过了 16.67ms(60fps 下),下一帧的 VSync 信号来临时,上一个阶段还没跑完。Choreographer 不会打断正在执行的代码,它只会跳过当前这一帧,等下一轮再继续。这就是丢帧的直接原因。

帧率目标 单帧预算 常见超时原因
60 fps 16.67 ms 频繁 GC、XML 布局膨胀、Bitmap 解码
90 fps 11.11 ms 列表滑动中的复杂 item 绘制
120 fps 8.33 ms 画面中大量半透明叠加 + 硬件加速瓶颈

注意:并不是所有掉帧都能直接看 Choreographer 日志。SystemServer 的 Watchdog 会杀进程,如果主线程阻塞超过 5 秒,会直接报 ANR。所以掉帧多了,下一步就是 ANR。

2.5 核心关系图

下面这张图展示了 Choreographer 在整个渲染管线中的位置。从底层的 HWC(硬件合成器)发出 VSync 信号,到最终屏幕上显示,一共经过 4 层。

Choreographer 帧调度流程 硬件 VSync 信号 Choreographer.doFrame() 开始 INPUT 处理触摸事件 ANIMATION 计算动画属性 TRAVERSAL measure / layout / draw RenderThread + HWC 屏幕显示 硬件每 16.67ms 发出一次 Choreographer 排队等待同步

2.6 监控驱动——实际项目怎么做

光知道原理不够,还得落地监控。我一般建议三步:

  • 打点统计:用 Choreographer.FrameCallback 记录每次 doFrame 的延时,算掉帧率。
  • 阈值报警:连续 3 帧超过 16ms 就上报一次。不要每帧都上报,否则后台会骂你。
  • 结合 Trace:掉帧时 dump 当前主线程的堆栈,看看是哪个方法在吃时间。

一个我踩过的坑:千万不要在 FrameCallback 里做 I/O 操作——比如写日志、发网络请求。我自己就在调试时干过这事,结果掉帧更严重了……

Choreographer 的机制其实不复杂。你只要记住它就是一个“准时干活”的调度器,而我们要做的就是保证每个阶段的活别干太久。后面几章会讲怎么精准定位到那个“太久”的方法。