Skip to content

FrameHandler 帧调度

从 FrameDisplayEventReceiver 到 FrameHandler 和 doFrame,追踪 VSYNC 消息、回调队列、阶段顺序、取消与跳帧边界。

基于android-17.0.0_r1
AndroidChoreographerFrameHandlerVSYNC源码阅读

FrameHandler 帧调度 ​

Choreographer 的 FrameHandler 不是一个“每 16 ms 自动运行的 Handler”。在 Android 17 中,它负责消费三类内部消息:执行无 VSYNC 模式的 frame、从 Looper 线程调用 scheduleVsync()、以及让延迟 callback 到期后重新申请 frame。真实的显示 VSYNC 由 FrameDisplayEventReceiver 接收,再把事件封装成异步 Message 投递给同一个 Handler。

本文面向已经读过 ViewRootHandler 绘制路径、 异步 Handler 工厂、消息延迟边界 和 异步消息与 VSYNC 的读者。本文聚焦 Choreographer.java 内部的消息和回调编排:Looper、FrameHandler、DisplayEventReceiver、 CallbackQueue 和 doFrame() 如何分工;不把 SurfaceFlinger 的 VSYNC 产生机制或完整绘制 渲染器实现塞入本文。

读完后,读者应能定位 VSYNC 事件何时进入 Handler,解释为什么消息要标记异步,判断延迟 callback 为什么先进入 CallbackQueue 而不是直接执行,并从 doFrame() 的状态检查、跳帧和 回调回收路径理解一帧没有执行、推迟或异常时的边界。

1. 实例归属 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

每个已有 Looper 的线程通过 ThreadLocal 获取自己的 Choreographer。构造函数保存 Looper, 创建绑定该 Looper 的 FrameHandler 和 FrameDisplayEventReceiver,再创建五个回调队列。

java
private Choreographer(Looper looper, long layerHandle) {
    mLooper = looper;
    mHandler = new FrameHandler(looper);
    mDisplayEventReceiver = USE_VSYNC
            ? new FrameDisplayEventReceiver(looper, layerHandle)
            : null;
    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
}

FrameHandler 的线程归属来自构造参数的 Looper;FrameDisplayEventReceiver 也绑定同一 Looper。它们不创建 worker 线程,VSYNC 到达后的 Java doFrame() 仍由该 Looper 线程执行。

对象所有者读取/调用者状态
mHandlerChoreographerFrameDisplayEventReceiver、延迟 callback消费内部 Message
mDisplayEventReceiverChoreographernative display event 入口接收 VSYNC
mCallbackQueuesChoreographerpostCallback、doCallbacks保存按阶段的 CallbackRecord
mFrameScheduledChoreographerscheduleFrameLocked、doFrame是否已有待处理帧

2. 内部消息 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

FrameHandler 只处理三个 what:MSG_DO_FRAME、MSG_DO_SCHEDULE_VSYNC、 MSG_DO_SCHEDULE_CALLBACK。它不是公开业务 Handler;每个 case 都调用 Choreographer 的内部 方法。

java
private static final int MSG_DO_FRAME = 0;
private static final int MSG_DO_SCHEDULE_VSYNC = 1;
private static final int MSG_DO_SCHEDULE_CALLBACK = 2;

private final class FrameHandler extends Handler {
    public FrameHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_DO_FRAME:
                doFrame(System.nanoTime(), 0,
                        new DisplayEventReceiver.VsyncEventData());
                break;
            case MSG_DO_SCHEDULE_VSYNC:
                doScheduleVsync();
                break;
            case MSG_DO_SCHEDULE_CALLBACK:
                doScheduleCallback(msg.arg1);
                break;
        }
    }
}

MSG_DO_FRAME 是无 VSYNC fallback 或特殊模式的入口;正常 VSYNC 会把 FrameDisplayEventReceiver 本身作为 Runnable 放进 Message.callback,不一定使用这个 what。所以不能仅凭三个常量概括所有 frame 入口。

3. 回调阶段 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

Choreographer 为五个阶段建立独立 CallbackQueue:INPUT、ANIMATION、 INSETS_ANIMATION、TRAVERSAL、COMMIT。阶段编号被 doFrame() 以固定顺序调用,但每个 队列内部仍按 dueTime 排序。

java
public static final int CALLBACK_INPUT = 0;
public static final int CALLBACK_ANIMATION = 1;
public static final int CALLBACK_INSETS_ANIMATION = 2;
public static final int CALLBACK_TRAVERSAL = 3;
public static final int CALLBACK_COMMIT = 4;

private final CallbackQueue[] mCallbackQueues =
        new CallbackQueue[CALLBACK_LAST + 1];

阶段顺序是 Choreographer 的内部调用顺序,不是 Handler 消息优先级。所有阶段都由同一个 Looper 线程串行执行;消息屏障只影响 VSYNC Message 何时进入 Looper,不会让阶段并行。

4. 回调入队 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

postCallbackDelayed() 先校验 action 和阶段,再在 mLock 内计算 dueTime,把动作放入 对应 CallbackQueue。如果动作已经到期,立即请求 frame;如果是未来时间,发送异步的 MSG_DO_SCHEDULE_CALLBACK,由它在到期后检查并请求 frame。

java
public void postCallbackDelayed(int callbackType,
        Runnable action, Object token, long delayMillis) {
    if (action == null) {
        throw new IllegalArgumentException("action must not be null");
    }
    if (callbackType < 0 || callbackType > CALLBACK_LAST) {
        throw new IllegalArgumentException("callbackType is invalid");
    }
    postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

延迟 Message 不承载真正的 callback 执行,而是承载“到期后是否需要申请 frame”的提示。 真正的 action 仍在 CallbackQueue 中,等 doFrame() 对应阶段提取。

这个分层解释了延迟帧 callback 的实际语义:延迟到期只让 callback 有资格在下一帧被提取, 不保证 action 在 dueTime 的同一毫秒开始执行。

5. 消息入口 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

当 scheduleFrameLocked() 在非 Looper 线程被调用时,它不能直接操作 DisplayEventReceiver,于是发送异步的 MSG_DO_SCHEDULE_VSYNC 到 FrameHandler;Looper 线程 取到后再调用 doScheduleVsync()。如果已经在 Looper 线程,则直接申请 VSYNC。

java
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
            if (isRunningOnLooperThreadLocked()) {
                scheduleVsyncLocked();
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } else {
            final long nextFrameTime = Math.max(
                    mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
            Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, nextFrameTime);
        }
    }
}

mFrameScheduled 在这里承担去重:多次 post callback 只申请一个尚未消费的 frame。异步位让 内部调度消息不受 ViewRootImpl traversal barrier 阻挡,但不改变它们仍需在 Choreographer Looper 上串行执行的事实。

6. VSYNC 转发 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

FrameDisplayEventReceiver.onVsync() 收到 native 事件后,保存时间戳、frame 编号和 VsyncEventData,再把自己作为 Runnable 封装进 Message。消息的时间使用 VSYNC 时间戳转换 后的 uptime 时间,并显式设置异步位。

java
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
        VsyncEventData vsyncEventData) {
    long now = System.nanoTime();
    if (timestampNanos > now) {
        timestampNanos = now;
    }

    if (mHavePendingVsync) {
        // 记录重复 pending 的异常情况
    } else {
        mHavePendingVsync = true;
    }

    mTimestampNanos = timestampNanos;
    mFrame = frame;
    mLastVsyncEventData.copyFrom(vsyncEventData);
    Message msg = Message.obtain(mHandler, this);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

onVsync() 可能来自 native 显示事件线程,但 run() 在 Handler 绑定的 Looper 线程执行。 mHavePendingVsync 和时间字段的写入配合“事件只允许一个 pending”逻辑,防止连续 VSYNC 事件无界堆积。

java
@Override
public void run() {
    mHavePendingVsync = false;
    doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
}

这里的 Runnable 身份很关键:Handler 分发时会走 Message.callback.run(),而不是进入 FrameHandler.handleMessage(MSG_DO_FRAME)。因此正常 VSYNC 和 fallback frame 是两条不同的 消息入口,但最终都汇聚到 doFrame()。

7. 帧状态 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

doFrame() 首先根据 buffer stuffing 状态决定是否延迟一个 frame,再在锁内检查 mFrameScheduled。如果没有待处理 frame,方法记录后直接返回;如果 frame 时间倒退或 FPS divisor 要求跳过,也会重新申请 VSYNC 后返回。

java
void doFrame(long frameTimeNanos, int frame,
        DisplayEventReceiver.VsyncEventData vsyncEventData) {
    switch (updateBufferStuffingState(frameTimeNanos, vsyncEventData)) {
        case DELAY_FRAME:
            mBufferStuffingState.numberWaitsForNextVsync++;
            mBufferStuffingState.accumulatedDelayNanos += vsyncEventData.frameInterval;
            scheduleVsyncLocked();
            return;
        default:
            break;
    }

    synchronized (mLock) {
        if (!mFrameScheduled) {
            traceMessage("Frame not scheduled");
            return;
        }
        // 计算 jitter、更新 frame data,并把 mFrameScheduled 清零
    }
}

mFrameScheduled 清零发生在确认 frame 可执行之后。它不是“VSYNC 已到达”标志,而是 “当前 Choreographer 是否仍有 callback 工作需要消费”的状态。VSYNC 到达但 callback 已被 移除时,doFrame() 可以无工作返回。

8. 跳帧判断 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

doFrame() 用当前 System.nanoTime() 与原始 VSYNC 时间计算 jitter。如果晚于一个 frame interval,就调整 frame time;跳过的帧数达到 SKIPPED_FRAME_WARNING_LIMIT 时输出警告。

java
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= frameIntervalNanos) {
    final long skippedFrames = jitterNanos / frameIntervalNanos;
    if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
        Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                + "The application may be doing too much work on its main thread.");
    }
    // 对 frameTimeNanos 重同步
}

这个分支说明 FrameHandler 不能保证准时:Message 到达 Looper 后,主线程可能已经错过 VSYNC。日志是对 jitter 的诊断,不是对某个具体 View 或 callback 的根因证明;要定位根因仍 需结合各阶段 callback、Looper 日志和主线程栈。

9. 阶段执行 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

一帧确认可执行后,doFrame() 按阶段调用 doCallbacks():输入、动画、insets 动画、 traversal、commit。阶段间使用同一个 mFrameData 和动画时钟;没有 callback 的阶段会直接 返回。

java
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

mFrameInfo.markInputHandlingStart();
doCallbacks(CALLBACK_INPUT);

mFrameInfo.markAnimationsStart();
doCallbacks(CALLBACK_ANIMATION);
doCallbacks(CALLBACK_INSETS_ANIMATION);

mFrameInfo.markPerformTraversalsStart();
doCallbacks(CALLBACK_TRAVERSAL);

doCallbacks(CALLBACK_COMMIT);

图中 CallbackQueue 是 callback 的真正 owner;FrameHandler 只负责把 frame 入口交给 Choreographer。阶段顺序不是五条 Handler 消息,而是同一次 doFrame() 中的五次队列提取。

10. 队列提取 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

CallbackQueue 是按 dueTime 排序的单链表。extractDueCallbacksLocked() 一次摘下所有 已到期记录,把第一条未到期记录留在队头;之后 doCallbacks() 在锁外运行这些 action。

java
public CallbackRecord extractDueCallbacksLocked(long now) {
    CallbackRecord callbacks = mHead;
    if (callbacks == null || callbacks.dueTime > now) {
        return null;
    }

    CallbackRecord last = callbacks;
    CallbackRecord next = last.next;
    while (next != null) {
        if (next.dueTime > now) {
            last.next = null;
            break;
        }
        last = next;
        next = next.next;
    }
    mHead = next;
    return callbacks;
}

回调执行期间可以再向同一阶段或后续阶段注册 callback;因为提取已经在锁内完成,新增记录 不会插入当前链表片段。doCallbacks() 完成后,finally 会把已执行记录逐个回收到 callback pool,避免长期持有 action 引用。

java
try {
    for (CallbackRecord c = callbacks; c != null; c = c.next) {
        c.run(mFrameData);
    }
} finally {
    synchronized (mLock) {
        do {
            final CallbackRecord next = callbacks.next;
            recycleCallbackLocked(callbacks);
            callbacks = next;
        } while (callbacks != null);
    }
}

11. 回调异常 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

doCallbacks() 的 finally 负责回收当前已经提取的 CallbackRecord,但它没有在循环内捕获 action 抛出的任意异常。一个 action 抛异常时,后续同一阶段的 action 不会从该循环继续执行, 但已提取记录仍会进入 finally 回收。

java
try {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
    for (CallbackRecord c = callbacks; c != null; c = c.next) {
        c.run(mFrameData);
    }
} finally {
    // mCallbacksRunning=false,并回收 callbacks 链
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

这与 FrameHandler.handleMessage() 的三路分发不同:FrameHandler 不为 doFrame() 建立 Future,也不把 callback 异常转换成普通结果。最终异常仍会进入 Looper 的线程级异常处理, 不能把“CallbackRecord 已回收”理解成“帧已经成功完成”。

12. 回调取消 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

取消分为两层:先从 CallbackQueue 删除匹配 action/token,再在 action 非空且 token 为空时, 移除对应的 MSG_DO_SCHEDULE_CALLBACK。这样未来 callback 的提示消息和真正 callback 记录 可以同时被取消。

java
private void removeCallbacksInternal(int callbackType, Object action, Object token) {
    synchronized (mLock) {
        mCallbackQueues[callbackType].removeCallbacksLocked(action, token);
        if (action != null && token == null) {
            mHandler.removeMessages(MSG_DO_SCHEDULE_CALLBACK, action);
        }
    }
}

public void removeFrameCallback(FrameCallback callback) {
    if (callback == null) {
        throw new IllegalArgumentException("callback must not be null");
    }
    removeCallbacksInternal(CALLBACK_ANIMATION, callback, FRAME_CALLBACK_TOKEN);
}

如果 doCallbacks() 已经把记录摘出,后续 remove 不能撤回正在运行的 action;取消只覆盖 仍在 CallbackQueue 或延迟调度 Message 中的工作。这与 Handler removeCallbacks() 的出队 竞态相似,但 Choreographer 还多了一层自己的 CallbackRecord 队列。

13. 无 VSYNC 路径 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

当 USE_VSYNC 为 false,scheduleFrameLocked() 发送异步 MSG_DO_FRAME,在计算出的下一 帧时间调用 FrameHandler.handleMessage();此时不会通过 DisplayEventReceiver 接收真实 VSYNC,但仍使用同一个 doFrame() 和 callback 阶段。

java
final long nextFrameTime = Math.max(
        mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);

因此 FrameHandler 不是“只处理 VSYNC 回调”的类;它是 Choreographer 的 Looper 入口,既 消费真实显示事件转发,也消费无 VSYNC fallback。两条路径在 doFrame() 汇合后才共享帧状态 和阶段队列。

14. 延迟阶段 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

到期的 MSG_DO_SCHEDULE_CALLBACK 只在尚未有 frame 预约时检查对应队列;如果有到期 callback, 调用 scheduleFrameLocked(now)。如果 frame 已经预约,消息不会重复申请第二帧。

java
void doScheduleCallback(int callbackType) {
    synchronized (mLock) {
        if (!mFrameScheduled) {
            final long now = SystemClock.uptimeMillis();
            if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
                scheduleFrameLocked(now);
            }
        }
    }
}

这条路径解释了“延迟 callback 到期但没有立刻运行”的原因:到期只触发 frame 申请,action 还需等 VSYNC 或 fallback frame 的对应阶段。dueTime 是 callback 的资格时间,不是 FrameCallback.doFrame() 的开始时间。

15. 时间与数据 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

VSYNC 事件同时携带纳秒时间戳和 VsyncEventData。doFrame() 会计算 jitter、更新 FrameData,并在 callback 中提供 frame time、vsync id 和 deadline 等数据。数据的有效范围 由 callback 契约限定,不应在 callback 之外持有引用推断未来帧。

java
public interface VsyncCallback {
    void onVsync(@NonNull FrameData data);
}

FrameData 的时间线选择和 buffer stuffing recovery 会影响 doFrame() 使用的 frame time; 这不是简单的“收到 timestamp 就原样传给所有回调”。阅读性能问题时,应区分原始 VSYNC 时间、 重同步后的 frame time 和 callback 实际开始时间。

16. 验证输入 ​

源码文件:frameworks/base/core/java/android/view/Choreographer.java

第一组验证是 callback 阶段和取消:向指定 callback type 加入一个 action,调用 removeCallbacks(type, action, token),再检查该 action 不会从对应 CallbackQueue 提取。 同时应检查延迟 callback 的 MSG_DO_SCHEDULE_CALLBACK 被按 action 移除。

java
choreographer.postCallbackDelayed(
        Choreographer.CALLBACK_ANIMATION, action, token, 1000);
choreographer.removeCallbacks(
        Choreographer.CALLBACK_ANIMATION, action, token);

这组输入证明的是 Choreographer 自己的记录和提示消息取消,不证明已经执行的 callback 会被 中断。

第二组验证是帧去重:连续 post 两个 callback,确认 mFrameScheduled 只导致一次 frame 申请,而 doFrame() 在对应阶段提取两个到期记录。它证明的是预约合并,不证明两个 callback 会在相同耗时或相同硬件帧时间完成。

第三组验证是异常回收:让一个 callback 抛出异常,观察 doCallbacks() 的 finally 是否将 已提取 CallbackRecord 回收到 pool,并确认外层 Looper 的异常策略仍然可见。没有设备 VSYNC 时,可用 USE_VSYNC=false 的测试配置验证 MSG_DO_FRAME fallback,但不能把 fallback 时间精度外推到真实显示刷新率。

17. 复查命令 ​

源码文件:

  • frameworks/base/core/java/android/view/Choreographer.java
  • frameworks/base/core/java/android/view/ViewRootImpl.java
  • frameworks/base/core/java/android/os/Handler.java
  • frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
bash
rg -n "class FrameHandler|MSG_DO_FRAME|MSG_DO_SCHEDULE_VSYNC|MSG_DO_SCHEDULE_CALLBACK" \
  frameworks/base/core/java/android/view/Choreographer.java

rg -n "postCallbackDelayedInternal|scheduleFrameLocked|doScheduleVsync|doScheduleCallback" \
  frameworks/base/core/java/android/view/Choreographer.java

rg -n "FrameDisplayEventReceiver|onVsync|setAsynchronous|doFrame" \
  frameworks/base/core/java/android/view/Choreographer.java

rg -n "doCallbacks|extractDueCallbacksLocked|removeCallbacksInternal|mFrameScheduled" \
  frameworks/base/core/java/android/view/Choreographer.java

阅读一帧时应按“callback 入队 → frame 预约 → VSYNC Message → doFrame 状态检查 → 阶段提取 → record 回收”的顺序走源码。把 postCallback 直接等同于 Runnable 入 Handler 队列, 会漏掉 CallbackQueue 的 dueTime、阶段和取消语义。

18. 使用边界 ​

Choreographer 的 FrameHandler 适合把输入、动画、insets、traversal 和 commit callback 组织到显示帧阶段。它不提供通用延迟执行器,不保证 callback 在 dueTime 精确开始,不会让 主线程工作并行,也不会在 callback 抛异常后继续执行同一阶段剩余 action。

当出现掉帧或 callback 未执行时,按源码检查:消息是否进入正确 Looper,mFrameScheduled 是否被提前清除,VSYNC 是否已转成异步 Message,CallbackQueue 的 dueTime 是否到期,callback 是否在 doCallbacks() 已被摘出,以及 doFrame() 是否因 jitter、时间倒退或 buffer recovery 重新申请了下一帧。这样才能区分预约缺失、消息等待、阶段未到、取消成功和主线程过载。