2. Choreographer 的帧率机制:每帧的调度与绘制
2.1 它到底在做什么?
Choreographer 翻译过来叫“编舞者”。名字挺文艺,干的却是硬核的活——协调输入、动画、绘制这三件事,让它们在一个垂直同步(VSync)周期里按顺序跑完。
我个人习惯把它理解成“Android 的节拍器”。你想想看,如果每个线程自己想什么时候刷新就什么时候刷新,画面肯定会撕裂或者跳帧。Choreographer 就是那个喊“1、2、3、开始”的人。
核心职责:
- 接收底层硬件的 VSync 信号
- 管理三种 callback 队列(输入、动画、绘制)
- 按固定顺序触发回调,保证一帧内的所有工作在下一帧到来前完成
2.2 一帧到底怎么走?
我来画个流程:VSync 信号一到,Choreographer 就依次调用三个阶段的 callback:
- INPUT(输入处理)——处理触摸事件、按键事件
- ANIMATION(动画计算)——执行所有活跃的动画帧,更新属性值
- 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 层。
2.6 监控驱动——实际项目怎么做
光知道原理不够,还得落地监控。我一般建议三步:
- 打点统计:用
Choreographer.FrameCallback记录每次doFrame的延时,算掉帧率。 - 阈值报警:连续 3 帧超过 16ms 就上报一次。不要每帧都上报,否则后台会骂你。
- 结合 Trace:掉帧时 dump 当前主线程的堆栈,看看是哪个方法在吃时间。
一个我踩过的坑:千万不要在 FrameCallback 里做 I/O 操作——比如写日志、发网络请求。我自己就在调试时干过这事,结果掉帧更严重了……
Choreographer 的机制其实不复杂。你只要记住它就是一个“准时干活”的调度器,而我们要做的就是保证每个阶段的活别干太久。后面几章会讲怎么精准定位到那个“太久”的方法。