6. Callback 管理机制
Choreographer 的回调管理是其核心设计之一,涉及回调的注册、存储、调度、执行和回收的完整生命周期。这一机制与第 4 节的消息机制紧密配合,共同构成了帧调度的基础。
6.1 回调存储结构
Choreographer
│
├─ mCallbackQueues: CallbackQueue[5] ← 5 个队列,对应 5 种回调类型
│ │
│ ├─ CallbackQueue (INPUT) ← 按 dueTime 排序的单向链表
│ │ └─ CallbackRecord → CallbackRecord → null
│ │
│ ├─ CallbackQueue (ANIMATION)
│ │ └─ CallbackRecord → CallbackRecord → CallbackRecord → null
│ │
│ ├─ CallbackQueue (INSETS_ANIMATION)
│ ├─ CallbackQueue (TRAVERSAL)
│ └─ CallbackQueue (COMMIT)
│
└─ mCallbackPool: CallbackRecord ← 对象池,复用已回收的节点
└─ CallbackRecord → CallbackRecord → null
每个 CallbackQueue 是一个按 dueTime 升序排列的单向链表 。dueTime 相同的回调按插入顺序排列(FIFO)。mCallbackPool 是一个空闲链表,回收的 CallbackRecord 节点放回池中,下次注册时直接复用,避免 GC。
6.2 CallbackRecord 详解
java
private static final class CallbackRecord {
public CallbackRecord next; // 链表指针
public long dueTime; // 到期时间(uptimeMillis 时基)
public Object action; // 回调动作(Runnable / FrameCallback / VsyncCallback)
public Object token; // 动作类型标识
}
token 的作用 :区分不同类型的回调动作,决定 run() 时如何分发:
| token | action 类型 | run() 调用方式 |
|---|---|---|
FRAME_CALLBACK_TOKEN |
Choreographer.FrameCallback |
callback.doFrame(frameTimeNanos) |
VSYNC_CALLBACK_TOKEN |
Choreographer.VsyncCallback |
callback.onVsync(frameData) |
null |
Runnable |
runnable.run() |
run 方法的两个重载:
java
// 传统方式:只传 frameTimeNanos
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback) action).doFrame(frameTimeNanos);
} else {
((Runnable) action).run();
}
}
// 新方式:传完整 FrameData(支持 VsyncCallback)
void run(FrameData frameData) {
frameData.setInCallback(true); // 开启访问控制
if (token == VSYNC_CALLBACK_TOKEN) {
((VsyncCallback) action).onVsync(frameData);
} else {
run(frameData.getFrameTimeNanos()); // 回退到传统方式
}
frameData.setInCallback(false); // 关闭访问控制
}
setInCallback(true/false) 在回调执行前后切换,确保 FrameData / FrameTimeline 的 getter 方法只能在回调内调用,回调外访问抛出 IllegalStateException。
6.3 CallbackQueue 详解
java
private final class CallbackQueue {
private CallbackRecord mHead; // 链表头(dueTime 最小的节点)
}
核心方法:
addCallbackLocked(dueTime, action, token) --- 有序插入:
java
// 从池中获取或新建 CallbackRecord
CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
if (mHead == null) {
mHead = callback; // 空链表,直接插入
return;
}
if (dueTime < mHead.dueTime) {
callback.next = mHead; // 比头还早,插入头部
mHead = callback;
return;
}
// 遍历找到合适位置,保持 dueTime 升序
while (entry.next != null) {
if (dueTime < entry.next.dueTime) {
callback.next = entry.next;
break;
}
entry = entry.next;
}
entry.next = callback;
extractDueCallbacksLocked(now) --- 批量提取到期回调:
java
// 从头部开始,提取所有 dueTime <= now 的节点
CallbackRecord callbacks = mHead;
if (callbacks == null || callbacks.dueTime > now) {
return null; // 无到期回调
}
// 找到到期与未到期的分界点,断开链表
CallbackRecord last = callbacks;
CallbackRecord next = last.next;
while (next != null && next.dueTime <= now) {
last = next;
next = next.next;
}
last.next = null; // 断开到期部分
mHead = next; // 未到期部分成为新头部
return callbacks; // 返回到期回调链表
hasDueCallbacksLocked(now) --- 检查是否有到期回调:
java
return mHead != null && mHead.dueTime <= now;
由于链表按 dueTime 升序排列,只需检查头节点即可判断是否有到期回调,O(1) 复杂度。
removeCallbacksLocked(action, token) --- 按 action + token 移除:
java
// 遍历链表,同时匹配 action 和 token
for (CallbackRecord callback = mHead; callback != null;) {
final CallbackRecord next = callback.next;
if ((action == null || callback.action == action)
&& (token == null || callback.token == token)) {
// 匹配成功,从链表中移除并回收
if (predecessor != null) {
predecessor.next = next;
} else {
mHead = next;
}
recycleCallbackLocked(callback); // 放回对象池
} else {
predecessor = callback;
}
callback = next;
}
6.4 对象池机制
java
// 获取:从池中取或新建
private CallbackRecord obtainCallbackLocked(long dueTime, Object action, Object token) {
CallbackRecord callback = mCallbackPool;
if (callback == null) {
callback = new CallbackRecord(); // 池空,新建
} else {
mCallbackPool = callback.next; // 从池中取出
callback.next = null;
}
callback.dueTime = dueTime;
callback.action = action;
callback.token = token;
return callback;
}
// 回收:清空数据,放回池中
private void recycleCallbackLocked(CallbackRecord callback) {
callback.action = null;
callback.token = null;
callback.next = mCallbackPool;
mCallbackPool = callback;
}
为什么用对象池 :Choreographer 每帧可能注册和执行数十个回调(动画、遍历、提交等),高频分配 CallbackRecord 会产生大量短命对象,增加 GC 压力。对象池将分配开销分摊到初始化阶段,运行时零分配。
6.5 回调与消息机制的配合
回调管理与消息机制在三个关键环节配合:
环节 1:注册时触发帧请求
postCallback(type, action, token)
│
├─ addCallbackLocked() ← 回调加入对应类型的 CallbackQueue
│
└─ dueTime <= now?
├─ yes → scheduleFrameLocked() ← 立即请求帧
└─ no → MSG_DO_SCHEDULE_CALLBACK(delay) ← 延迟到到期再请求
回调注册到 CallbackQueue 后,通过消息机制触发 scheduleFrameLocked() 请求 vsync。延迟回调通过 MSG_DO_SCHEDULE_CALLBACK 延迟触发。
环节 2:延迟回调到期检查
MSG_DO_SCHEDULE_CALLBACK 到期
│
└─ doScheduleCallback(callbackType)
├─ 检查 !mFrameScheduled(避免重复请求)
└─ hasDueCallbacksLocked(now) ← 检查该类型队列是否有到期回调
├─ yes → scheduleFrameLocked() ← 有到期回调,请求帧
└─ no → 不请求(回调可能已被移除)
延迟消息到期后,通过 hasDueCallbacksLocked 确认队列中确实有到期回调,避免无效帧请求。
环节 3:doFrame 中提取并执行
doFrame()
│
├─ doCallbacks(CALLBACK_INPUT)
│ ├─ extractDueCallbacksLocked(now) ← 从 INPUT 队列提取到期回调
│ ├─ 遍历执行 CallbackRecord.run(frameData)
│ └─ recycleCallbackLocked() ← 回收到对象池
│
├─ doCallbacks(CALLBACK_ANIMATION)
│ └─ ...(同上)
│
├─ ...(INSETS_ANIMATION, TRAVERSAL, COMMIT)
│
└─ 注意:早期阶段可以在回调中 post 新回调,
后续阶段会在同一帧的 doCallbacks 中提取执行
关键设计:跨阶段注册 。doCallbacks 使用 System.nanoTime() 作为提取时间(行 971),而非帧开始时间。这意味着 ANIMATION 阶段 post 的 TRAVERSAL 回调,会在同一帧的 TRAVERSAL 阶段被提取执行(因为 dueTime <= now 已满足)。这是支持"输入事件触发动画 → 动画触发遍历"链式更新的关键。
6.6 回调生命周期总结
注册 存储 调度 执行 回收
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
postCallback() addCallbackLocked scheduleFrameLocked doCallbacks recycleCallbackLocked
│ │ │ │
│ ├─ dueTime<=now ├─ extract ├─ action=null
│ │ → 直接请求 │ DueLocked │ ├─ token=null
│ │ │ │ └─ 放回 mCallbackPool
│ └─ dueTime>now │
│ → MSG_DO_ ├─ run(frameData)
│ SCHEDULE_ │ ├─ VsyncCallback
│ CALLBACK │ ├─ FrameCallback
│ → doSchedule │ └─ Runnable
│ Callback │
│ → hasDue │
│ Callbacks │
│ Locked │
│ → schedule │
│ FrameLocked │
▼ ▼
CallbackQueue CallbackRecord.run()
(按 dueTime 排序)
7. Jitter 修正机制
7.1 第一次修正(doFrame 中)
时机 :vsync 理论开始时间 → doFrame 实际开始执行之间。
触发条件 :jitterNanos >= frameIntervalNanos(延迟超过 1 帧)。
修正方式:
java
lastFrameOffset = jitterNanos % frameIntervalNanos
frameTimeNanos = startNanos - lastFrameOffset
将帧时间对齐到最近的 vsync 边界,减少动画抖动。
关键设计 :传给 mFrameData.update() 的 jitterNanos 是原始值 (非 lastFrameOffset),因为 deadline 搜索需要真实的延迟量来找到有效的未来 timeline。
7.2 第二次修正(CALLBACK_COMMIT 中)
时机 :doFrame 开始 → COMMIT 回调执行之间(前面所有阶段消耗的时间)。
触发条件 :jitterNanos >= 2 * frameIntervalNanos(延迟超过 2 帧)。
修正方式:
java
lastFrameOffset = jitterNanos % frameIntervalNanos + frameIntervalNanos
frameTimeNanos = now - lastFrameOffset
比第一次修正多加一个 frameIntervalNanos,确保 commit 时间始终至少落后一帧。
为什么更保守:下一帧可能已被调度,commit 时间不能接近当前时间,否则会导致下一帧的 frameTime <= 当前 commit 时间,触发"Frame time goes backward"检查。
7.3 两次修正对比
| doFrame 修正 | COMMIT 修正 | |
|---|---|---|
| 延迟来源 | vsync → doFrame 启动 | doFrame → COMMIT 执行 |
| 触发阈值 | 1 帧 | 2 帧 |
| offset 计算 | jitter % interval |
jitter % interval + interval |
| 修正目标 | 对齐 vsync 边界 | 对齐到至少前一帧,保证单调递增 |
7.4 与 ValueAnimator 的协作
ValueAnimator 通过 AnimationHandler 注册两种回调:
- ANIMATION 阶段 :
doAnimationFrame(frameTime)--- 记录mLastFrameTime,设置mStartTime - COMMIT 阶段 :
commitAnimationFrame(frameTime)--- 计算调整量并修正mStartTime
java
// ValueAnimator.commitAnimationFrame()
long adjustment = frameTime - mLastFrameTime;
if (adjustment > 0) {
mStartTime += adjustment; // 推后起始时间,防止跳帧
}
当 COMMIT 未触发修正时(jitter < 2 帧),frameTime == mLastFrameTime,adjustment = 0,自然 no-op。只有严重延迟时才补偿,避免动画跳帧。
8. mFrameScheduled 的状态流转
idle (false)
│
│ postCallback / scheduleFrameLocked
▼
scheduled (true) ──→ vsync 到达 → doFrame → false (清除)
│ │
│ 帧时间倒退 │ 正常处理完
│ scheduleVsyncLocked (重新请求) │ 回到 idle
▼
scheduled (true) ──→ 下一帧...
设计目的:单次触发的锁存器,保证"一帧只请求一次 vsync",防止多个 callback post 导致重复 vsync 请求。
9. 内部类
9.1 FrameHandler
java
private final class FrameHandler extends Handler
Choreographer 的消息处理器,处理 3 种消息(见第 4 节)。继承自 Handler,与 Choreographer 的 Looper 绑定。
9.2 FrameDisplayEventReceiver
java
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable
Vsync 事件接收器,是 Choreographer 与 SurfaceFlinger 之间的桥梁。
关键机制:
onVsync()被 native 层回调时,不直接执行doFrame,而是将自身作为Runnablepost 到 Handler- 使用
sendMessageAtTime(msg, timestampNanos)让消息在 vsync 时间点执行 - 这防止 vsync 事件饿死消息队列中已有的消息
字段:
| 字段 | 说明 |
|---|---|
mHavePendingVsync |
防止重复 vsync 事件堆积 |
mTimestampNanos |
vsync 时间戳 |
mFrame |
帧序号 |
mLastVsyncEventData |
缓存的 vsync 数据 |
9.3 CallbackRecord
java
private static final class CallbackRecord
回调记录,链表节点。通过对象池(mCallbackPool)复用,避免 GC。详见第 6 节的 Callback 管理机制。
| 字段 | 说明 |
|---|---|
next |
链表指针 |
dueTime |
到期时间(uptimeMillis) |
action |
回调动作(Runnable / FrameCallback / VsyncCallback) |
token |
动作类型标识(FRAME_CALLBACK_TOKEN / VSYNC_CALLBACK_TOKEN / null) |
run 方法根据 token 分发(详见第 6.2 节):
FRAME_CALLBACK_TOKEN→FrameCallback.doFrame(frameTimeNanos)VSYNC_CALLBACK_TOKEN→VsyncCallback.onVsync(frameData)- 其他 →
Runnable.run()
9.4 CallbackQueue
java
private final class CallbackQueue
按 dueTime 排序的单向链表队列。每个 CallbackQueue 对应一种回调类型。详见第 6 节的 Callback 管理机制。
| 方法 | 说明 |
|---|---|
addCallbackLocked() |
按 dueTime 有序插入 |
extractDueCallbacksLocked() |
提取所有到期回调,从链表中移除 |
hasDueCallbacksLocked() |
检查是否有到期回调(O(1),只查头节点) |
removeCallbacksLocked() |
按 action + token 移除回调 |
9.5 FrameTimeline
java
public static class FrameTimeline
描述一个可能的 VSync 帧呈现时间线。包含:
vsyncId--- 对应的 vsync 标识,用于 HWUI 与 SurfaceFlinger 关联expectedPresentationTimeNanos--- 预期呈现时间deadlineNanos--- 帧完成截止时间
访问控制 :通过 mInCallback 标志限制只能在回调内访问,回调外抛出 IllegalStateException。
9.6 FrameData
java
public static class FrameData
VsyncCallback 的载荷,包含帧信息和多个可选时间线。
为什么使用 FrameTimeline 数组:
- SurfaceFlinger 在每次 Vsync 时提供多个按时间排序的呈现时间线
- 应用可根据延迟需求选择合适的 timeline(需要更多渲染时间选较晚的,追求低延迟选最早的)
- 掉帧时在已有数组中查找有效 timeline,避免昂贵的 binder 调用
mPreferredFrameTimelineIndex标记系统推荐的最优 timeline
update 方法(3 个重载):
| 方法 | 用途 |
|---|---|
update(frameTimeNanos, vsyncEventData) |
正常更新,从 vsync 数据填充所有 timeline |
update(frameTimeNanos, receiver, jitterNanos) |
Jitter 修正,在数组中搜索 deadline 有效的 timeline |
update(frameTimeNanos, newPreferredIndex) |
仅更新帧时间和首选索引 |
9.7 FrameCallback / VsyncCallback
java
public interface FrameCallback {
void doFrame(long frameTimeNanos);
}
public interface VsyncCallback {
void onVsync(@NonNull FrameData data);
}
FrameCallback:传统接口,只提供帧时间(纳秒)VsyncCallback:新接口,提供完整的FrameData(含多时间线、deadline、presentation time),支持更精细的帧调度
10. 外部类
10.1 DisplayEventReceiver
java
public abstract class DisplayEventReceiver
Choreographer 与 SurfaceFlinger 之间的底层通信桥梁。通过 JNI 与 native 层交互。
主要职责:
scheduleVsync()--- 向 SurfaceFlinger 请求下一次 vsync 脉冲(单次触发)onVsync()--- native 层回调,收到 vsync 事件时触发getLatestVsyncEventData()--- 通过 binder 从 SF 获取最新 vsync 数据(较慢,仅用于 jitter 修正 fallback)dispose()--- 释放 native 资源
Vsync 源类型:
| 常量 | 值 | 说明 |
|---|---|---|
VSYNC_SOURCE_APP |
0 | App vsync(Choreographer 默认使用) |
VSYNC_SOURCE_SURFACE_FLINGER |
1 | SF vsync(getSfInstance() 使用) |
VsyncEventData 内部类 :包含帧间隔 frameInterval、多个 FrameTimeline(最多 7 个)、首选索引。由 native 层填充。
线程安全:非线程安全,所有方法只能在创建时的 Looper 线程调用。
10.2 FrameInfo
java
public final class FrameInfo // android.graphics.FrameInfo
帧时序信息记录,用于 jank 追踪和性能分析。使用 long[] 紧凑存储,便于 JNI 传递给 HWUI。
记录的时间点:
| 索引 | 字段 | 记录时机 |
|---|---|---|
| 0 | FLAGS | 帧标志(窗口可见性变化等) |
| 1 | FRAME_TIMELINE_VSYNC_ID | vsync ID |
| 2 | INTENDED_VSYNC | 理论 vsync 时间(未修正) |
| 3 | VSYNC | 实际使用的 vsync 时间(jitter 修正后) |
| 4 | INPUT_EVENT_ID | 触发帧的输入事件 ID |
| 5 | HANDLE_INPUT_START | 输入处理开始时间 |
| 6 | ANIMATION_START | 动画计算开始时间 |
| 7 | PERFORM_TRAVERSALS_START | 遍历开始时间 |
| 8 | DRAW_START | 绘制开始时间 |
| 9 | FRAME_DEADLINE | 帧完成截止时间 |
| 10 | FRAME_START_TIME | 帧实际开始时间 |
| 11 | FRAME_INTERVAL | 帧间隔 |
在 Choreographer 中的使用:
setVsync()--- doFrame 开始时记录 vsync 信息markInputHandlingStart()--- 输入处理前markAnimationsStart()--- 动画计算前markPerformTraversalsStart()--- 遍历开始前
通过相邻时间点的差值可推断各阶段耗时,例如 DRAW_START - PERFORM_TRAVERSALS_START = layout 耗时。
11. 回调类型与执行顺序
CALLBACK_INPUT (0) → 输入事件分发
↓
CALLBACK_ANIMATION (1) → ValueAnimator / ObjectAnimator 动画计算
↓
CALLBACK_INSETS_ANIMATION (2) → WindowInsets 动画进度更新
↓
CALLBACK_TRAVERSAL (3) → measure / layout / draw
↓
CALLBACK_COMMIT (4) → 后绘制操作,帧时间最终修正
为什么 ANIMATION 在 TRAVERSAL 之前:动画计算会改变 View 属性(位置、透明度等),必须在 layout/draw 之前完成,确保遍历时使用最新的属性值。
为什么 INSETS_ANIMATION 单独分离 :需要先收集所有 inset 动画更新,再统一分发 dispatchWindowInsetsAnimationProgress,避免多次触发遍历。
为什么 COMMIT 在最后 :它需要等待 traversal 完成,才能知道帧的实际消耗时间,从而决定是否修正帧时间。修正后的帧时间提供给 ValueAnimator.commitAnimationFrame() 用于调整动画起始时间。
12. 公开 API
12.1 帧回调
| 方法 | 说明 |
|---|---|
postFrameCallback(FrameCallback) |
注册下一帧回调(ANIMATION 阶段) |
postFrameCallbackDelayed(FrameCallback, delay) |
延迟注册 |
removeFrameCallback(FrameCallback) |
移除回调 |
postVsyncCallback(VsyncCallback) |
注册 vsync 回调(ANIMATION 阶段,提供完整 FrameData) |
removeVsyncCallback(VsyncCallback) |
移除 vsync 回调 |
12.2 通用回调(@hide)
| 方法 | 说明 |
|---|---|
postCallback(type, action, token) |
注册指定类型回调 |
postCallbackDelayed(type, action, token, delay) |
延迟注册 |
removeCallbacks(type, action, token) |
移除回调 |
12.3 时间查询
| 方法 | 说明 |
|---|---|
getFrameTime() |
当前帧时间(ms,uptimeMillis 时基),仅回调内可用 |
getFrameTimeNanos() |
当前帧时间(ns,nanoTime 时基),仅回调内可用 |
getLastFrameTimeNanos() |
上一帧时间,任何时候可用 |
getFrameIntervalNanos() |
帧间隔(刷新周期) |
getExpectedPresentationTimeNanos() |
预期呈现时间(ns) |
getExpectedPresentationTimeMillis() |
预期呈现时间(ms) |
getLatestExpectedPresentTimeNanos() |
最新预期呈现时间(涉及 binder 调用,慎用) |
getVsyncId() |
当前帧 vsync ID |
getFrameDeadline() |
当前帧截止时间 |
12.4 帧延迟控制
| 方法 | 说明 |
|---|---|
getFrameDelay() / setFrameDelay(long) |
非 vsync 模式下的帧间隔 |
subtractFrameDelay(long) |
从延迟中减去帧延迟(补偿 16ms 假设) |
13. 设计要点总结
- 单 Vsync 请求 :
mFrameScheduled确保一帧只请求一次 vsync,避免重复 IPC - 对象池复用 :
CallbackRecord通过mCallbackPool复用,避免高频分配导致的 GC 压力 - 异步消息:所有帧相关消息都设为 asynchronous,避免被同步屏障阻挡
- 两级 Jitter 修正:doFrame 修正启动延迟,COMMIT 修正执行延迟,保证帧时间单调递增
- 多时间线设计:FrameTimeline 数组支持可变刷新率和掉帧时的优雅降级
- 回调访问控制 :
mInCallback标志防止回调外访问无效的帧数据 - 线程隔离:ThreadLocal 保证每线程独立实例,主线程实例额外缓存
- 动画时钟锁定 :
AnimationUtils.lockAnimationClock()在 doFrame 期间冻结动画时钟,确保所有回调看到一致的时间