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时间戳,单位是纳秒。你可以在回调里用它来计算动画进度,或者判断是否发生了掉帧。
回调的注册流程是这样的:
- 调用
postFrameCallback,把callback加入mCallbackQueues[CALLBACK_ANIMATION]队列 - 如果当前没有正在处理的帧,调用
scheduleVsyncLocked向底层请求VSync - VSync信号到达,
onVsync被触发,开始执行doFrame 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的整体调度流程:
从图里你能看到,整个流程是串行的。VSync信号触发onVsync,然后进入doFrame,再依次处理三个队列。任何一个队列卡住,后面的队列都得等。
嗯,这里要注意一点:doFrame的执行是在主线程的Handler消息循环里。如果主线程有其他消息正在处理,doFrame就得排队。这就是为什么主线程不能做耗时操作——它直接影响每一帧的调度。
最后说一个我踩过的坑。有一次做驱动适配,我发现VSync信号明明准时到了,但应用层就是收不到。追查下去,发现是FrameDisplayEventReceiver的onVsync方法里做了异常捕获,但捕获后没有重新请求VSync。结果就是——只收到一次VSync,后续所有帧都卡住了。修复方案很简单:在onVsync的finally块里重新调用scheduleVsync。
核心总结:Choreographer的本质是一个VSync驱动的任务调度器。它把输入、动画、绘制三种任务统一到一个时间片里,按优先级串行执行。理解了这个模型,你就能明白为什么主线程卡顿会导致掉帧,为什么VSync信号丢失会导致画面静止。