Skip to content

ViewRootHandler 绘制路径

从 requestLayout、invalidate 和窗口回调追踪 ViewRootHandler 的消息、同步屏障、VSYNC 协作与主线程消费边界。

基于android-17.0.0_r1
AndroidViewRootImplChoreographerMessageQueue源码阅读

ViewRootHandler 绘制路径 ​

ViewRootImpl.ViewRootHandler 不是“所有 View 操作的总开关”,也不是一个独立渲染线程。它是 ViewRootImpl 持有的普通 Handler:消息在创建 ViewRootImpl 的 Looper 上执行,绘制请求由 scheduleTraversals() 去重后插入同步屏障,再交给 Choreographer 的 VSYNC 回调触发。

本文面向已经读过 ActivityThread.H 主线程、 异步消息与 VSYNC、同步屏障 和 Handler消息发送 的读者。本文选取三条源码 路径:requestLayout()/invalidate() 如何汇聚到 traversal,窗口和输入回调如何进入 ViewRootHandler,以及消息、屏障、VSYNC 和 performTraversals() 的生效顺序。它不展开 performTraversals() 内部全部 measure/layout/draw 算法,也不把每个 MSG_* 常量当成独立专题。

读完后,读者应能定位 ViewRootHandler 的 owner 和 Looper,解释为什么多个 requestLayout() 不会在同一帧重复注册 traversal,判断普通消息为什么可能被屏障延后,以及从 WMS resize 或 InputEventReceiver 入口找到主线程消费和失败清理路径。

1. 对象归属 ​

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

ViewRootImpl 构造时把 mHandler 传给 View.AttachInfo,同时获取当前线程的 Choreographer。因此 ViewRootHandler、ViewRootImpl 状态、View 树和 Choreographer 都围绕 创建 ViewRootImpl 的 Looper 组织,而不是由 Handler 自己创建线程。

java
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
        context);
mChoreographer = Choreographer.getInstance();

ViewRootImpl 还把同一个 mHandler 用于显示监听、无障碍监听等回调,并把它包装成 HandlerExecutor 供需要 Executor 的 API 使用;这说明 Handler 的 owner 是 ViewRootImpl, 消费者可以是 View 系统或其他 framework 回调。

java
DisplayManagerGlobal
        .getInstance()
        .registerDisplayListener(
                mDisplayListener,
                mHandler,
                eventsToBeRegistered,
                mBasePackageName);
对象所有者主要消费者线程来源
ViewRootImpl窗口视图根traversal、窗口状态、输入队列创建 ViewRootImpl 的线程
mHandlerViewRootImplViewRootHandler 消息分支mLooper
mChoreographer当前 LooperVSYNC callback同一 Looper
mViewViewRootImplmeasure/layout/draw、输入分发UI 线程

2. 消息入口 ​

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

ViewRootHandler 继承普通 Handler,使用 getMessageName() 给消息提供可读名称,并在 handleMessage() 外层统一进行 View trace。真正的 case 分支位于 handleMessageImpl(),这样 Trace 的结束可以由 finally 保证。

java
final class ViewRootHandler extends Handler {
    @Override
    public String getMessageName(Message message) {
        switch (message.what) {
            case MSG_INVALIDATE:
                return "MSG_INVALIDATE";
            case MSG_RESIZED:
                return "MSG_RESIZED";
            case MSG_DISPATCH_INPUT_EVENT:
                return "MSG_DISPATCH_INPUT_EVENT";
        }
        return super.getMessageName(message);
    }

    @Override
    public void handleMessage(Message msg) {
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, getMessageName(msg));
        }
        try {
            handleMessageImpl(msg);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
}

这里的 finally 只保证 trace 配对,不会捕获并吞掉 handleMessageImpl() 的业务异常。消息 仍然在 Looper 的普通 Handler 分发边界中执行,异常传播和线程退出要继续回到 Looper 源码。

3. 布局请求 ​

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

requestLayout() 的最终目标是 scheduleTraversals()。该方法先检查线程,再用 mTraversalScheduled 做去重;只有从“未安排”变为“已安排”时,才插入屏障并注册 VSYNC 回调。

java
void scheduleTraversals() {
    checkThreadCompat();
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        postTraversalBarrier();
        mChoreographer.postVsyncCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalCallback);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

mTraversalScheduled 是 ViewRootImpl 的状态,不是 MessageQueue 的去重。两个 View 在同一帧 内分别调用 requestLayout(),都可能沿父链到达 ViewRootImpl,但第二次只看到标志已设置, 不会再插入第二个 traversal barrier 或注册第二个 callback。

3.1 线程边界 ​

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

ViewRootImpl 通过 checkThreadCompat() 约束需要 UI 线程的入口;它不是把任意线程调用自动 转发到 mHandler。错误线程是否抛出或记录,受当前兼容变更和检查配置影响。

java
void checkThread() {
    Thread current = Thread.currentThread();
    if (mThread != current) {
        throwCalledFromWrongThreadException();
    }
}

private void throwCalledFromWrongThreadException() {
    throw newCalledFromWrongThreadException(/*withInitStack=*/true);
}

这和 postInvalidate() 是两条不同的 API 契约:需要修改 ViewRootImpl 状态的布局请求要求 原始线程;跨线程的无效区域请求则显式封装成 Handler 消息。

4. 屏障注册 ​

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

postTraversalBarrier() 把同步屏障 token 保存在 ViewRootImpl。当前实现还支持原子屏障字段, 在重复设置时负责移除被替换的旧 token,避免 barrier 泄漏。

java
private void postTraversalBarrier() {
    int newBarrier = mQueue.postSyncBarrier();
    if (mAtomicTraversalBarrier) {
        final long oldBarrier = mTraversalBarrierAtomic.getAndSet(newBarrier);
        if (oldBarrier != UNSET_TRAVERSAL_BARRIER) {
            mQueue.removeSyncBarrier((int) oldBarrier);
        }
    } else {
        mTraversalBarrier = newBarrier;
    }
}

private void removeTraversalBarrier() {
    if (mAtomicTraversalBarrier) {
        final long oldBarrier = mTraversalBarrierAtomic.getAndSet(UNSET_TRAVERSAL_BARRIER);
        if (oldBarrier != UNSET_TRAVERSAL_BARRIER) {
            mQueue.removeSyncBarrier((int) oldBarrier);
        }
    } else {
        mQueue.removeSyncBarrier(mTraversalBarrier);
    }
}

屏障的 owner 是 ViewRootImpl,MessageQueue 只保存屏障节点。屏障的生效时机从注册开始, 直到 doTraversal() 移除;它不是永久优先级,也不是把所有消息停止,而是让同步消息在 屏障之后等待异步消息或屏障移除。

5. VSYNC 注册 ​

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

屏障注册后,ViewRootImpl 把 traversal callback 交给 Choreographer。此处的 callback 不是 mHandler.postDelayed(),而是 Choreographer 管理的 VSYNC 调度;后续由 callback 进入 doTraversal(frameTimeNanos)。

java
mChoreographer.postVsyncCallback(
        Choreographer.CALLBACK_TRAVERSAL, mTraversalCallback);

这条路径把三个状态分开:

状态owner生效时机
mTraversalScheduledViewRootImpl第一次 schedule 时写入
traversal barrierMessageQueue/ViewRootImpl token注册后阻挡同步消息
VSYNC callbackChoreographer下一个显示帧调度阶段

所以“调用 requestLayout() 后立刻 measure”不符合源码顺序。调用只把一次 traversal 标记为 待执行;真正的测量和布局要等待 Choreographer 的 frame 回调。

这张图表达的是 traversal 的 owner 关系,不表示 Choreographer 直接调用 ViewRootHandler。 在 Android 17 的实现中,ViewRootImpl 的 traversal callback 由 Choreographer 帧阶段调用, ViewRootHandler 仍负责其他窗口和输入消息。

6. 遍历执行 ​

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

doTraversal() 只在状态仍为已安排时继续:先清除标志,移除屏障,再进入 performTraversals(frameTimeNanos)。这保证一次 callback 消费掉当前预约,并释放同步消息 继续处理的条件。

java
void doTraversal(long frameTimeNanos) {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        removeTraversalBarrier();
        performTraversals(frameTimeNanos);
    }
}

这里的顺序很重要:屏障在 performTraversals() 之前移除,而不是绘制结束后才移除。否则 traversal 期间可能阻塞需要在绘制逻辑中重新入队的同步消息。performTraversals() 具体如何 测量窗口、调用 View 树和提交绘制,属于 View 系统的更深专题;本文只把它作为 traversal 消费者定位。

7. 无效区域 ​

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

跨线程的 postInvalidate 路径不直接访问 ViewRootImpl 的布局状态,而是把 View 或 InvalidateInfo 放入 Handler 消息。延迟参数最终由 Handler 的 sendMessageDelayed() 转换 成 Message.when。

java
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
    Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
    mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

public void dispatchInvalidateRectDelayed(AttachInfo.InvalidateInfo info,
        long delayMilliseconds) {
    final Message msg = mHandler.obtainMessage(MSG_INVALIDATE_RECT, info);
    mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

消息到达后,ViewRootHandler 调用目标 View 的 invalidate(),或者处理区域信息并回收 InvalidateInfo。消息本身不直接调用 performTraversals();无效化会通过 View 父链再次 汇聚到 traversal 调度。

java
case MSG_INVALIDATE:
    ((View) msg.obj).invalidate();
    break;
case MSG_INVALIDATE_RECT:
    final View.AttachInfo.InvalidateInfo info =
            (View.AttachInfo.InvalidateInfo) msg.obj;
    info.target.invalidate(info.left, info.top, info.right, info.bottom);
    info.recycle();
    break;

7.1 取消路径 ​

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

cancelInvalidate() 按消息类型、Handler 和对象匹配移除尚未取出的消息,同时清理 mInvalidateOnAnimationRunnable 中的记录。它不能撤回已经从 MessageQueue 摘出的消息。

java
public void cancelInvalidate(View view) {
    mHandler.removeMessages(MSG_INVALIDATE, view);
    mHandler.removeMessages(MSG_INVALIDATE_RECT, view);
    mInvalidateOnAnimationRunnable.removeView(view);
}

因此取消成功只表示队列中匹配项被移除;如果消息已经进入 handleMessageImpl(),业务代码 可能已经调用了 View.invalidate。需要更强取消语义时,必须在调用方维护自己的状态标志。

8. 输入消息 ​

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

输入事件消息在投递点显式标记为异步,保证 traversal barrier 存在时仍能被 MessageQueue 寻找。它仍然在 ViewRootHandler 所属 Looper 上串行执行,不会由异步位创建输入线程。

java
public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
    SomeArgs args = SomeArgs.obtain();
    args.arg1 = event;
    args.arg2 = receiver;
    Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args);
    msg.setAsynchronous(true);
    mHandler.sendMessage(msg);
}

消息消费先把事件加入输入队列,再回收 SomeArgs;后续输入处理由 doProcessInputEvents()/输入队列继续负责。

java
case MSG_DISPATCH_INPUT_EVENT: {
    SomeArgs args = (SomeArgs) msg.obj;
    InputEvent event = (InputEvent) args.arg1;
    InputEventReceiver receiver = (InputEventReceiver) args.arg2;
    enqueueInputEvent(event, receiver, 0, true);
    args.recycle();
    break;
}

异步消息只是越过屏障的选择资格;如果主线程正在执行一个长时间的 performTraversals() 或 其他 callback,输入仍需等待当前分发结束。这是“绕过屏障”和“抢占主线程”的区别。

9. 窗口回调 ​

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

窗口系统通过内部 W 接收 resize 回调。W 持有 ViewRootImpl 的 WeakReference,先判断 对象是否仍存活,再决定直接处理还是投递 MSG_RESIZED。当回调来自本地 transaction item 且线程就是 UI 线程时,可以直接调用 handleResized();其他情况进入 Handler。

java
static class W extends WindowClientTransactionHandler {
    private final WeakReference<ViewRootImpl> mViewAncestor;

    @Override
    public void resized(WindowRelayoutResult layout, boolean reportDraw, boolean forceLayout,
            int displayId, boolean syncWithBuffers, boolean dragResizing) {
        final boolean isFromResizeItem = mIsFromTransactionItem;
        mIsFromTransactionItem = false;
        final ViewRootImpl viewAncestor = mViewAncestor.get();
        if (viewAncestor == null) {
            return;
        }
        if (isFromResizeItem && viewAncestor.mLooper
                == ActivityThread.currentActivityThread().getLooper()) {
            viewAncestor.handleResized(layout.frames, reportDraw, layout.mergedConfiguration,
                    layout.insetsState, forceLayout, displayId, layout.syncSeqId,
                    syncWithBuffers, dragResizing, layout.activityWindowInfo);
            return;
        }
        viewAncestor.dispatchResized(layout, reportDraw, forceLayout, displayId,
                syncWithBuffers, dragResizing);
    }
}

直接路径的条件比“Binder 回调总要 Handler 转发”更窄。对象已被回收、线程不匹配或来源不是 transaction item 时,代码会走消息路径或直接返回。

10. 尺寸消息 ​

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

dispatchResized() 把窗口 relayout 结果和控制参数打包进 SomeArgs,根据 reportDraw 选择 MSG_RESIZED_REPORT 或 MSG_RESIZED,再发送给 mHandler。

java
private void dispatchResized(WindowRelayoutResult layout, boolean reportDraw,
        boolean forceLayout, int displayId, boolean syncWithBuffers, boolean dragResizing) {
    Message msg = mHandler.obtainMessage((reportDraw && !NoPreloadHolder.sAlwaysSeqId)
            ? MSG_RESIZED_REPORT : MSG_RESIZED);
    SomeArgs args = SomeArgs.obtain();
    args.arg1 = layout;
    args.argi1 = forceLayout ? 1 : 0;
    args.argi2 = displayId;
    args.argi3 = syncWithBuffers ? 1 : 0;
    args.argi4 = dragResizing ? 1 : 0;
    msg.obj = args;
    mHandler.sendMessage(msg);
}

消费时先判断 mAdded,再调用 handleResized(),最后回收 SomeArgs。窗口已经 detach 时, 消息仍可能到达,但处理函数会通过状态条件阻止继续使用已失效的窗口状态。

java
case MSG_RESIZED:
case MSG_RESIZED_REPORT: {
    final SomeArgs args = (SomeArgs) msg.obj;
    final WindowRelayoutResult layout = (WindowRelayoutResult) args.arg1;
    final boolean reportDraw = msg.what == MSG_RESIZED_REPORT;
    handleResized(layout.frames, reportDraw, layout.mergedConfiguration,
            layout.insetsState, args.argi1 != 0, args.argi2, layout.syncSeqId,
            args.argi3 != 0, args.argi4 != 0, layout.activityWindowInfo);
    args.recycle();
    break;
}

11. 清理消息 ​

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

ViewRootHandler 还处理 MSG_DIE、insets、focus 和窗口移动等消息。以 MSG_DIE 为例, 消息消费者是 doDie(),而不是直接释放 Handler;ViewRootImpl 的窗口、监听器、Renderer 和 ViewRoot 状态由更大的 detach/cleanup 路径负责。

java
case MSG_DIE: {
    doDie();
    break;
}

如果设置窗口失败,setView() 的异常路径会把 View 引用和 root 关联清空,调用 unscheduleTraversals(),再抛出窗口异常。这个清理顺序说明 traversal barrier 不是“最终一定 会由 VSYNC callback 移除”;窗口创建失败时必须主动取消。

java
} catch (RemoteException | RuntimeException e) {
    mView = null;
    mAttachInfo.mRootView = null;
    mFallbackEventHandler.setView(null);
    unscheduleTraversals();
    setAccessibilityFocus(null, null);
    throw new RuntimeException("Adding window failed", e);
}

12. 线程检查 ​

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

ViewRootImpl 保存创建线程 mThread;checkThread() 发现不一致时构造 CalledFromWrongThreadException。这解释了为什么普通 View 修改不是自动发送到 ViewRootHandler,而是要求调用者使用 UI 线程或显式 post/postInvalidate API。

java
void checkThread() {
    Thread current = Thread.currentThread();
    if (mThread != current) {
        throwCalledFromWrongThreadException();
    }
}

private CalledFromWrongThreadException newCalledFromWrongThreadException(boolean withInitStack) {
    return new CalledFromWrongThreadException(
            "Only the original thread that created a view hierarchy can touch its views."
                    + " Expected: " + mThread.getName()
                    + " Calling: " + Thread.currentThread().getName(),
            withInitStack ? mInitStack : null);
}

此异常证明的是调用线程违反 ViewRootImpl 线程契约,不证明 MessageQueue 没有消息,也不证明 把同一调用包装成任意 Handler 消息就能安全执行;消息中的对象状态仍需在目标线程有效。

13. 验证输入 ​

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

第一组验证可检查 traversal 去重:在同一个 ViewRootImpl 上连续触发两次 scheduleTraversals(), 观察 mTraversalScheduled 只从 false 变 true 一次;第二次不会重复插入屏障。该验证依赖 ViewRootImpl 的测试可见性或源码级 instrumentation,不能仅用普通应用 API 断言内部 token。

text
初始:mTraversalScheduled = false
第一次 schedule:写 true,postTraversalBarrier,postVsyncCallback
第二次 schedule:发现 true,不再注册新的 callback

第二组验证直接围绕消息载荷:调用 dispatchInvalidateDelayed(view, delay),然后在队列中 检查 what == MSG_INVALIDATE、obj == view;取消前调用 cancelInvalidate(view),断言匹配 消息不再存在。它证明的是 Handler 消息边界,不证明 View 已经完成下一次绘制。

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

第三组验证使用 TransactionExecutor/ViewRoot 集成测试时,应分别安排窗口 resize、窗口 detach 和输入消息,断言:已 detach 的 ViewRoot 不继续处理 resize 状态;输入消息的 SomeArgs 被回收;trace 或 barrier 清理不会留下待执行 traversal。没有完整设备环境时, 这些结论应停留在源码分支和单元测试层,不外推为所有硬件渲染器行为。

14. 复查命令 ​

源码文件:

  • frameworks/base/core/java/android/view/ViewRootImpl.java
  • frameworks/base/core/java/android/view/View.java
  • frameworks/base/core/java/android/view/Choreographer.java
  • frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
bash
rg -n "class ViewRootHandler|MSG_INVALIDATE|MSG_RESIZED|MSG_DISPATCH_INPUT_EVENT" \
  frameworks/base/core/java/android/view/ViewRootImpl.java

rg -n "scheduleTraversals|postTraversalBarrier|removeTraversalBarrier|doTraversal" \
  frameworks/base/core/java/android/view/ViewRootImpl.java

rg -n "dispatchInvalidateDelayed|cancelInvalidate|dispatchInputEvent|dispatchResized" \
  frameworks/base/core/java/android/view/ViewRootImpl.java

rg -n "requestLayout|postInvalidate|invalidateInternal" \
  frameworks/base/core/java/android/view/View.java

阅读一条 UI 更新路径时,顺序应是:View 的状态变化或窗口回调 → ViewRootImpl 状态/消息 → 同步屏障与 Choreographer → doTraversal() → performTraversals()。如果中间插入“Handler 直接执行 draw”,就已经偏离了当前源码。

15. 使用边界 ​

ViewRootHandler 的价值在于把 ViewRootImpl 的窗口、输入和失效消息串到正确的 Looper; scheduleTraversals() 则把 layout/draw 请求合并到下一次 frame。它不提供跨线程修改 View 的 自动安全性,不让输入抢占正在运行的 callback,也不保证窗口 detach 后旧消息自动消失。

排查“调用了 requestLayout 但没有马上绘制”时,应依次检查:调用线程是否符合 ViewRootImpl 线程契约,mTraversalScheduled 是否已经为 true,屏障是否成功登记,Choreographer callback 是否仍在队列,doTraversal() 是否移除屏障,以及 performTraversals() 前的窗口状态是否 允许继续。这样才能把“没有请求”“等待帧”“消息被取消”和“窗口已失效”区分开。