【阅读源码-Android】动画之Choreographer-2

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 == mLastFrameTimeadjustment = 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,而是将自身作为 Runnable post 到 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_TOKENFrameCallback.doFrame(frameTimeNanos)
  • VSYNC_CALLBACK_TOKENVsyncCallback.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 数组

  1. SurfaceFlinger 在每次 Vsync 时提供多个按时间排序的呈现时间线
  2. 应用可根据延迟需求选择合适的 timeline(需要更多渲染时间选较晚的,追求低延迟选最早的)
  3. 掉帧时在已有数组中查找有效 timeline,避免昂贵的 binder 调用
  4. 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. 设计要点总结

  1. 单 Vsync 请求mFrameScheduled 确保一帧只请求一次 vsync,避免重复 IPC
  2. 对象池复用CallbackRecord 通过 mCallbackPool 复用,避免高频分配导致的 GC 压力
  3. 异步消息:所有帧相关消息都设为 asynchronous,避免被同步屏障阻挡
  4. 两级 Jitter 修正:doFrame 修正启动延迟,COMMIT 修正执行延迟,保证帧时间单调递增
  5. 多时间线设计:FrameTimeline 数组支持可变刷新率和掉帧时的优雅降级
  6. 回调访问控制mInCallback 标志防止回调外访问无效的帧数据
  7. 线程隔离:ThreadLocal 保证每线程独立实例,主线程实例额外缓存
  8. 动画时钟锁定AnimationUtils.lockAnimationClock() 在 doFrame 期间冻结动画时钟,确保所有回调看到一致的时间
相关推荐
mmsx1 小时前
基于 Android 的博物馆文化展示与预约 App —— 设计与实现(有源码和文档)
android·app·源码·文档
一笑的小酒馆2 小时前
AndroidAGP9.0升级之路
android
阮胜昌3 小时前
MySQL 中的动态数据脱敏:无需更改应用程序即可保护敏感数据
android·mysql·adb
aaajj5 小时前
关于MODIFY_PHONE_STATE权限
android
古法安卓5 小时前
Android-高版本 LMKD 源码解析:基于 PSI 的内存压力监控与查杀全流程
android·java·android studio
古法安卓6 小时前
Android-PSI 详解:libpsi 源码解析——116 行代码架起 lmkd 与内核之间的桥
android·java·android studio
mmsx9 小时前
Android 新闻资讯客户端实战:从零搭建一个完整的新闻 App
android·app·源码
三84410 小时前
PHP 绕过污点追踪构造 Webshell:反序列化引用复用与 Trait 可变函数 RCE
android
石头猫灯10 小时前
WordPress wp2shell漏洞断点逐过程到注入点
android·学习