3. Choreographer 工作流程:输入事件、动画、绘制与 FrameCallback
说实话,刚开始接触 Android 渲染的时候,我以为 Choreographer 就是个“定时器”,负责每隔 16ms 喊一声“该画了”。后来我翻源码才发现,这家伙的工作量远比我想象的复杂——它要协调输入事件、动画、绘制,还得给外部提供一个回调钩子。今天我就带你走一遍它的核心工作流。
3.1 整个流程长什么样?
Choreographer 的“心跳”由 VSync 信号驱动。每次 VSync 到来,它会依次做三件事:处理输入事件 → 执行动画回调 → 发起绘制。另外还提供一个 FrameCallback 机制,让你能在下一帧开始时拿到时间戳。下面这张图可以帮你快速建立全局印象。
3.2 第一步:输入事件处理(Input Event)
VSync 到达后,Choreographer 会首先调用 doFrame()。在这个方法里,它先处理的是 输入事件队列。为什么输入事件要放在最前面?你想想看,用户的触摸、按键等操作如果不先分发,后面的动画和绘制就是过时的数据。
我记得有一次在排查一个列表滑动掉帧问题,发现掉帧竟然不是因为绘制耗时,而是 输入事件处理阶段耗时太长。后来定位到是某个自定义 ViewGroup 在 onInterceptTouchEvent 里做了大量计算。这个阶段处理的,是 InputConsumer 从 InputTransport 读出来的事件列表。
| 阶段 | 核心方法 | 典型耗时原因 |
|---|---|---|
| 输入事件处理 | InputConsumer.consume() | 事件队列堆积、Touch 事件分发中的同步锁 |
| 动画回调 | AnimationUtils.lockClock() → Choreographer.doFrame | 属性动画计算、动画监听器回调 |
| 绘制 | ViewRootImpl.performTraversals() | measure/layout/draw 耗时、硬件加速缓冲提交 |
3.3 第二步:动画回调(Animation Callback)
输入事件处理完后,Choreographer 会取出 mAnimationCallbacks 队列,逐个执行。注意,这里的动画回调特指 通过 Choreographer 注册的帧动画,比如 ValueAnimator、ViewPropertyAnimator 等。它们依赖 VSync 来驱动帧更新。
// Choreographer 内部大致逻辑(简化)
void doFrame(long frameTimeNanos, int frame) {
// 1. 处理输入事件
doCallbacks(InputEventCallback, frameTimeNanos);
// 2. 处理动画回调
doCallbacks(AnimationCallback, frameTimeNanos);
// 3. 处理绘制回调
doCallbacks(TraversalCallback, frameTimeNanos);
// 4. 处理 FrameCallback
doCallbacks(FrameCallback, frameTimeNanos);
}
这里有个很容易被忽略的点:如果你在动画回调里 直接调用了 postInvalidate(),Choreographer 会把绘制请求排到下一帧,而不是立即执行。所以别指望动画回调里能触发同步绘制。
3.4 第三步:绘制与显示(Traversal & Draw)
动画回调执行完,Choreographer 处理绘制回调。这个阶段会触发 ViewRootImpl.performTraversals()。说白了这个方法就是老生常谈的 measure → layout → draw 三部曲。但要注意,一次 doFrame 只触发一次遍历,如果你在同一个 VSync 周期里多次调用 invalidate(),Choreographer 会合并请求,只执行一次。
绘制阶段是掉帧的重灾区。我自己的经验是:在 draw() 方法里避免创建对象、避免频繁的 Bitmap 解码、避免过度绘制(overdraw)。另外硬件加速虽然快,但 HardwareRenderer 提交到 GPU 的 buffer 如果太大,也会被卡住。
performTraversals() 可能会因为 布局大小没变、没有脏区域 而跳过绘制(直接返回)。但 Choreographer 仍然认为该帧被“处理”了。所以监控掉帧时,不能只看 doFrame 是否被调用,还要结合 FrameMetrics 中的 drawDuration 字段判断真正的绘制耗时。
3.5 FrameCallback 机制:你的帧时间戳“窃听器”
最后一个要聊的是 FrameCallback。这是一个非常实用的 API,你可以通过 Choreographer.getInstance().postFrameCallback(FrameCallback callback) 注册一个回调。回调的 doFrame(long frameTimeNanos) 方法会在 下一帧 VSync 到来后、绘制开始前 被调用。典型用法:
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
long nowNanos = System.nanoTime();
long frameIntervalMs = (nowNanos - lastFrameNanos) / 1_000_000;
if (frameIntervalMs > 16.67f) {
int droppedFrames = (int)(frameIntervalMs / 16.67f);
Log.w("FrameMonitor", "丢帧数: " + droppedFrames);
}
// 继续监听下一帧
if (isMonitoring) {
Choreographer.getInstance().postFrameCallback(this);
}
}
});
这个小工具是我做帧率监控的必备利器。我在项目中经常用它来统计连续 100 帧的平均耗时,发现掉帧问题就弹 Toast 提醒自己。不过要注意,frameTimeNanos 是 Choreographer 预计的 VSync 到达时间,而 System.nanoTime() 才是实际回调时间——两者之差就是这一帧的延迟。
FrameMetrics API(Android 7.0+)。它会按阶段拆解耗时,包括 Input、Animation、Layout、Draw、Submit 等。这样你就能精准定位究竟是哪个阶段拖慢了帧率。
3.6 小结:一个“会干活”的三段式流程
Choreographer 的工作流程,说白了就是:收到 VSync 信号 → 先处理用户输入(避免过时)→ 再驱动动画(让动画平滑)→ 最后执行绘制(生成新帧)。FrameCallback 就像是挂在流程上的一个钩子,让你在每一帧开始时获得控制权。
只要理解了这个流程,后面做掉帧分析时,你就知道该去检查哪个环节。比如滑动掉帧,优先查输入事件处理;动画卡顿,优先查动画回调里的计算;画面撕裂或绘制延迟,就从 performTraversals 入手。