3. Choreographer核心:创建、初始化与统一调度

Choreographer,这个词直译过来是“舞蹈编导”。在Android系统里,它干的活也差不多——协调所有跟画面刷新相关的动作。VSync信号一来,谁该上场、谁该准备、谁该退场,都由它说了算。

说实话,我刚接触这块源码时,第一反应是“这玩意儿怎么这么绕”。但后来我意识到,不理解Choreographer,你就没法真正理解Android的刷新机制。驱动适配做得再好,上层调度乱了,画面该掉帧还是掉帧。

3.1 Choreographer的创建与初始化

Choreographer是典型的一进程单例。每个线程只能有一个实例,通过getInstance()获取。内部用ThreadLocal来保证这一点。

// 伪代码,展示核心逻辑
public final class Choreographer {
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("...");
            }
            return new Choreographer(looper, VSYNC_SOURCE_APP);
        }
    };

    public static Choreographer getInstance() {
        return sThreadInstance.get();
    }
}

创建时,它需要绑定一个Looper。默认用当前线程的Looper。为什么?因为它内部要处理消息队列,所有回调最终都通过Handler投递到Looper所在的线程执行。

关键点:UI线程的Choreographer绑定的是主线程Looper。如果你在子线程调getInstance(),会创建一个属于子线程的Choreographer。但子线程没有Surface,没法直接刷新UI——这个坑我后面会讲。

初始化过程里,Choreographer会做三件重要的事:

  • 创建FrameHandler:用于处理MSG_DO_FRAME、MSG_DO_SCHEDULE_VSYNC等消息
  • 创建FrameDisplayEventReceiver:这是一个继承自DisplayEventReceiver的抽象类,底层通过JNI注册VSync监听
  • 初始化回调队列数组:一共三个队列,分别对应输入事件、动画、绘制

我记得第一次看FrameDisplayEventReceiver的源码时,找了半天没找到VSync信号到底从哪来的。后来追到nativeScheduleVsync才明白——它通过SurfaceFlinger的Composer服务注册了一个fd,VSync信号本质上是一个内核事件,通过epoll机制唤醒。

3.2 FrameCallback机制

FrameCallback是Choreographer对外暴露的核心接口。应用层通过postFrameCallback或postFrameCallbackDelayed注册一个回调,下一帧VSync到来时就会被调用。

public interface FrameCallback {
    public void doFrame(long frameTimeNanos);
}

这个frameTimeNanos参数很关键。它代表的是这一帧的VSync时间戳,单位是纳秒。你可以在回调里用它来计算动画进度,或者判断是否发生了掉帧。

回调的注册流程是这样的:

  1. 调用postFrameCallback,把callback加入mCallbackQueues[CALLBACK_ANIMATION]队列
  2. 如果当前没有正在处理的帧,调用scheduleVsyncLocked向底层请求VSync
  3. VSync信号到达,onVsync被触发,开始执行doFrame
  4. doFrame里按优先级依次处理三个队列:输入事件 → 动画 → 绘制

我的经验:FrameCallback的触发时机非常精确。我曾经用它来测量每一帧的执行耗时,发现如果doFrame里做了太多计算,下一帧的VSync就会错过。说白了,你在doFrame里待得越久,掉帧风险越大。

3.3 输入事件与动画的统一调度

这是Choreographer最巧妙的设计。它把三种看似不相关的事情——触摸事件处理、动画更新、View绘制——放在同一个时间片里处理。为什么?因为这三件事都跟画面刷新有关。

你想想看:用户触摸屏幕,产生一个触摸事件。这个事件如果不在下一帧开始前处理完,画面就没法及时响应。动画也是,每一帧都要计算新的属性值。绘制更是,所有计算结果最终都要体现在屏幕上。

Choreographer把这三件事按优先级排好队:

队列 常量 处理内容 优先级
输入事件 CALLBACK_INPUT 触摸、按键等事件的分发 最高
动画 CALLBACK_ANIMATION 属性动画、View动画的更新 中
绘制 CALLBACK_TRAVERSAL measure/layout/draw 最低

在doFrame方法里,它会依次调用doCallbacks(INPUT)、doCallbacks(ANIMATION)、doCallbacks(TRAVERSAL)。每个队列内部按FIFO顺序执行。

void doFrame(long frameTimeNanos, int frame) {
    // ... 掉帧检测逻辑省略
    
    // 1. 处理输入事件
    doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
    
    // 2. 处理动画
    doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
    
    // 3. 处理绘制
    doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
}

这个顺序不是随便定的。输入事件必须最先处理,因为用户交互的响应延迟直接影响体验。动画其次,因为动画计算依赖输入事件的结果。绘制最后,因为它要拿到所有计算结果才能画。

避坑指南:我曾经在一个项目里遇到奇怪的掉帧问题。排查了半天,发现是某个第三方库在CALLBACK_INPUT队列里做了大量Bitmap解码。输入事件队列本该是轻量级的,结果被它拖慢了,导致后续动画和绘制都延迟。记住:不要在输入回调里做耗时操作。

统一调度的另一个好处是——减少不必要的VSync请求。如果当前帧没有输入事件、没有动画、没有绘制请求,Choreographer就不会向底层请求VSync。这能省电,尤其是在静态页面场景下。

我画了一张图,帮你理清Choreographer的整体调度流程:

Choreographer 统一调度流程 VSync 信号 onVsync() doFrame() CALLBACK_INPUT 触摸/按键事件分发 CALLBACK_ANIMATION 属性动画/View动画更新 CALLBACK_TRAVERSAL measure/layout/draw 屏幕刷新 优先级:输入 > 动画 > 绘制 同一VSync周期内串行执行

从图里你能看到,整个流程是串行的。VSync信号触发onVsync,然后进入doFrame,再依次处理三个队列。任何一个队列卡住,后面的队列都得等。

嗯,这里要注意一点:doFrame的执行是在主线程的Handler消息循环里。如果主线程有其他消息正在处理,doFrame就得排队。这就是为什么主线程不能做耗时操作——它直接影响每一帧的调度。

最后说一个我踩过的坑。有一次做驱动适配,我发现VSync信号明明准时到了,但应用层就是收不到。追查下去,发现是FrameDisplayEventReceiver的onVsync方法里做了异常捕获,但捕获后没有重新请求VSync。结果就是——只收到一次VSync,后续所有帧都卡住了。修复方案很简单:在onVsync的finally块里重新调用scheduleVsync。

核心总结:Choreographer的本质是一个VSync驱动的任务调度器。它把输入、动画、绘制三种任务统一到一个时间片里,按优先级串行执行。理解了这个模型,你就能明白为什么主线程卡顿会导致掉帧,为什么VSync信号丢失会导致画面静止。