1. Choreographer 核心回调流程:doFrame 回调的触发时机、回调链组成、Vsync 信号与调度
1.1 doFrame 回调的触发时机
doFrame 是 Choreographer 帧处理流程的入口,每次系统产生垂直同步(Vsync)信号时被调用。其核心触发时机取决于 Vsync 信号到达 Choreographer 的消息队列。
- 硬件 Vsync 信号:由显示硬件(如 GPU/Display 驱动)以固定频率(通常为 60Hz 或 90Hz)产生。
- 软件 Vsync 转发:
SurfaceFlinger接收硬件 Vsync,通过VsyncThread分发到各个应用进程的Choreographer。 - 回调执行窗口:
doFrame在应用主线程(UI 线程)的消息队列中执行,必须在下一个 Vsync 信号到来之前完成,否则将导致丢帧。
关键概念:Vsync 信号不是直接触发 doFrame,而是通过 Choreographer 的消息入队机制,将 doFrame 包装为一个 Runnable,等待主线程 Looper 轮询处理。主线程阻塞会直接延迟 doFrame 的执行。
1.2 回调链组成:三大回调类型
Choreographer 内部维护三条回调链表,每种回调在 doFrame 的不同阶段被处理,优先级从高到低依次为:
| 回调类型 | 阶段标识 | 典型用途 | 执行顺序 |
|---|---|---|---|
INPUT |
CALLBACK_INPUT |
处理触摸事件、按键输入 | 第 1 位 |
ANIMATION |
CALLBACK_ANIMATION |
执行属性动画、视图动画更新 | 第 2 位 |
TRAVERSAL |
CALLBACK_TRAVERSAL |
执行 measure、layout、draw(View 树遍历) | 第 3 位(最终) |
回调链表结构:每条链表以 CallbackQueue 的形式存储,每个 CallbackQueue 内部又按 action(即 Runnable)的提交顺序排队。提交回调使用 postCallback API:
// 向 TRAVERSAL 队列提交帧回调
Choreographer.getInstance().postCallback(
Choreographer.CALLBACK_TRAVERSAL,
new Runnable() {
@Override
public void run() {
// 自定义帧回调逻辑,如 View.invalidate()
}
},
null
);
1.3 Vsync 信号与调度机制
Choreographer 的调度依赖于 FrameDisplayEventReceiver,它继承自 DisplayEventReceiver,通过 JNI 接收底层 Vsync 事件。
- Vsync 注册:
Choreographer在首次提交回调时,通过scheduleVsyncLocked()向底层注册 Vsync 监听。 - 事件到达:底层通过 Binder 或共享内存传递
vsync timestamp到FrameDisplayEventReceiver.onVsync()。 - 消息入队:
onVsync()将doFrame包装为一个Message,发送到主线程的Handler。 - 回调执行:主线程 Looper 轮询到该消息,调用
doFrame(long frameTimeNanos, int frame, ...)。
调度修正(FrameSkips):
- 如果
doFrame执行时发现frameTimeNanos已经落后当前时间超过一个帧周期(16ms),则丢弃本轮帧(stale frame),并立即触发下一次 Vsync 重试。 - 连续丢帧超过阈值(如 2 帧)会触发
JankStats记录卡顿。
1.4 完整回调流程 SVG 框架图
下图展示了从硬件 Vsync 到 doFrame 回调链执行的全路径:
1.5 常见错误与调试
陷阱 1:主线程耗时操作导致 doFrame 延迟
如果在主线程执行 I/O、大文件读写或复杂计算,即使 Vsync 信号准时到达,doFrame 也会因 Looper 队列排队而被延时。调试时可以使用
如果在主线程执行 I/O、大文件读写或复杂计算,即使 Vsync 信号准时到达,doFrame 也会因 Looper 队列排队而被延时。调试时可以使用
systrace 抓取主线程 Looper 状态:
// ADB 命令抓取 5 秒 trace 包含 Choreographer 标签
adb shell atrace --app=com.your.app -b 20480 Choreographer Input View -t 5 -o /data/local/tmp/trace.atrace
陷阱 2:回调链中抛异常导致后续回调中断
doFrame 内部是按顺序执行三条回调链,若 INPUT 或 ANIMATION 回调中抛出未捕获异常,会直接跳出 doFrame,导致 TRAVERSAL 队列被跳过,造成一帧完全丢失。可通过全局异常捕获或 UncaughtExceptionHandler 监控。
1.6 性能优化基线与测量指标
| 指标 | 正常范围 | 告警阈值 | 工具 |
|---|---|---|---|
| doFrame 回调耗时 | < 12ms | > 16ms(丢帧风险) | Choreographer.FrameCallback |
| Vsync 到 doFrame 延迟 | < 4ms | > 8ms(调度延迟) | systrace + FrameMetrics |
| TRAVERSAL 回调耗时 | < 8ms | > 12ms(View 过度绘制或低效布局) | Profile GPU Rendering |
实践建议:在
Activity 的 onCreate 中注册一个帧回调记录基准值:
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long mLastFrameTime = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (mLastFrameTime != 0) {
long delta = (frameTimeNanos - mLastFrameTime) / 1_000_000L;
// 理想 delta = 16.7ms,超过 16ms 即可能存在卡顿
}
mLastFrameTime = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});