Handler系列写到第三篇前两篇如果已经聊过 Looper 循环和 Message 入队这一篇的位置就非常明确了同步屏障Sync Barrier。这名字听起来很底层但它离我们其实不远——你每点一次屏幕、每次触发 View 重绘、每次 Choreographer 开始排帧背后都有它的身影。很多同学对 Handler 的理解止步于 sendMessage 和 handleMessage真到要解释“为什么明明发了一堆消息绘制却能插队”的时候就卡住了。这篇文章我会从 MessageQueue 源码出发把同步屏障的插入、跳过、移除三个阶段拆开讲再把 Choreographer 和 ViewRootImpl 里怎么用它讲清楚最后把我在实际调试中踩过的几个坑一并列出来。适合已经理解 Handler 基础消息流想往系统源码和 UI 性能方向再走一步的开发者。1. 同步屏障到底是什么1.1 target 为 null 的特殊消息在 MessageQueue 里普通消息一进队就要求必须有 target也就是发送它的 Handler。enqueueMessage里直接有这样的检查if (msg.target null) throw new IllegalArgumentException(Message must have a target.)。但同步屏障恰恰“违反”了这个规则它是一份target null的 Message被直接插到消息链表里像一个路障一样挡在队列中间。为什么不用 Handler 分发因为屏障本身不打算被任何回调处理它存在的唯一意义就是“卡位”。它靠什么定位自己靠arg1字段里存的一个整型 token。这个 token 由postSyncBarrier返回后续调用removeSyncBarrier(token)时就是靠它找到对应屏障。所以你可以把同步屏障理解成“一个占着队列位置、但没有处理者的消息”。正因为没有 targetnext()在遍历队列时会把它识别成特殊节点而不是普通任务。1.2 它到底在挡什么我先说结论同步屏障挡的是“屏障之后插入的同步消息”放行的是“屏障之后插入的异步消息”。用个生活化的比喻。消息队列就像一条单车道大家按到达时间排队。正常情况下谁先到谁先走这是 Handler 默认的公平机制。但有时候系统希望“某些任务”能插队比如这一帧的绘制任务必须优先执行不能排在用户输入、定时器、网络回调后面。这时候系统就在队列里放一个路障从此刻起普通住户同步消息被拦在路障后面只能排队等挂了特殊通行证的车辆异步消息可以穿过路障继续往前走。但要注意路障只对“从它后面来的消息”生效。路障之前已经在队列里的消息依然按原顺序正常执行。这很像你走进餐厅后厨前面已经下锅的菜不会撤回来新来的单子才需要插队规则。消息类型屏障前位置屏障后插入同步消息正常执行被挡下等待屏障移除异步消息正常执行可以越过屏障按时间顺序执行这套机制解决的核心问题就是 Android UI 的“帧提交窗口”问题。绘制和遍历这类任务必须抢在 VSYNC 信号之后尽快完成如果排在一条很长的普通消息队列后面轻则掉帧重则整个界面卡住不动。同步屏障就是系统用来给关键任务开“绿色通道”的手段。2. MessageQueue 源码里的同步屏障2.1 postSyncBarrier屏障是怎么插进去的源码里插入屏障的方法叫postSyncBarrier(long when)常见版本的核心逻辑是这样的private int postSyncBarrier(long when) { synchronized (this) { final int token mNextBarrierToken; final Message msg Message.obtain(); msg.markInUse(); msg.when when; msg.arg1 token; Message prev null; Message p mMessages; if (when ! 0) { while (p ! null p.when when) { prev p; p p.next; } } if (prev ! null) { msg.next p; prev.next msg; } else { msg.next p; mMessages msg; } return token; } }几个细节值得说。第一token 从mNextBarrierToken自增而来保证每次插入的屏障都有唯一标识。这个值一旦发出移除时就必须原样传回不然找不到。第二Message.obtain()拿到一个复用对象markInUse()是防止它被回收池重新分配。屏障消息的target始终保持 null这正是next()识别屏障的依据。第三when ! 0时按时间找插入点所以屏障不一定总在队列最前面。when传当前时间的时候它通常会插到队首传未来时间就会排在那些when更早的消息后面。这个细节很多人会忽略后面排查“为什么屏障没挡住某条消息”时经常是它引起的。插入屏障不需要唤醒队列因为队列本来就可能处于空闲等待中。真正需要唤醒的是removeSyncBarrier和enqueueMessage这一点后续再展开。2.2 next() 遇到屏障后怎么跳消息队列取出消息的核心方法是next()。普通情况下它把mMessages按when依次返回。但代码开头有一段专门处理屏障的逻辑Message next() { ... for (;;) { ... synchronized (this) { final long now SystemClock.uptimeMillis(); Message prevMsg null; Message msg mMessages; if (msg ! null msg.target null) { // 遇到同步屏障从屏障开始向后寻找异步消息 do { prevMsg msg; msg msg.next; } while (msg ! null !msg.isAsynchronous()); } if (msg ! null) { if (now msg.when) { nextPollTimeoutMillis (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { mBlocked false; if (prevMsg ! null) { prevMsg.next msg.next; } else { mMessages msg.next; } msg.next null; msg.markInUse(); return msg; } } else { // 屏障后面没有异步消息队列进入无限等待 nextPollTimeoutMillis -1; } ... } } }这里的do-while是整段代码的灵魂。当队列头部是屏障时prevMsg从屏障开始一路向后扫描直到找到第一个isAsynchronous() true的消息。找到后把它前面的节点可能是屏障也可能是一串同步消息直接跨过去让这条异步消息出队。如果屏障后面没有异步消息msg会变成 nullnextPollTimeoutMillis被设为 -1意味着nativePollOnce无限期睡下去。注意这时候即便队列里堆满了同步消息它们也出不来。只有两种情况能唤醒来了一条异步消息enqueueMessage里会调用nativeWake屏障被移除removeSyncBarrier会唤醒队列。这解释了为什么同步屏障能“卡死”一堆同步消息它们不是不执行而是被卡在屏障后面连出队的机会都没有。2.3 removeSyncBarrier拆墙屏障完成了使命必须及时拆掉。源码如下public void removeSyncBarrier(int token) { synchronized (this) { Message prev null; Message p mMessages; while (p ! null (p.target ! null || p.arg1 ! token)) { prev p; p p.next; } if (p null) { throw new IllegalStateException(The specified message queue synchronization barrier token has not been posted and has already been removed.); } // 从链表中摘除屏障节点 if (prev ! null) { prev.next p.next; } else { mMessages p.next; } p.recycleUnchecked(); // 唤醒可能阻塞在 next() 的线程 ... } }这里能看到一个关键特征p.target ! null的是普通消息直接跳过p.target null但arg1 ! token的是其他屏障也要跳过。所以队列里可以同时存在多个屏障它们各自用 token 区分移除时互不干扰。recycleUnchecked()把屏障消息送回对象池释放占用。这一步也很重要如果漏掉消息复用的数量会不断膨胀。移除屏障后必须唤醒线程因为next()可能正卡在nativePollOnce(-1)上不唤醒的话即便屏障移除了Looper 也不会立刻重新扫描队列。3. 异步 Handler 与异步消息的实际用法3.1 创建异步 Handler系统里最常见的做法是直接用一个async标记为 true 的 HandlerHandler asyncHandler new Handler(Looper.myLooper(), null, true);第三个参数就是async。这个 Handler 发出的所有消息在enqueueMessage阶段都会被自动打上异步标记。看一下 Handler 内部实现private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { msg.target this; msg.flags | mAsynchronous ? Message.FLAG_ASYNCHRONOUS : 0; return queue.enqueueMessage(msg, uptimeMillis); }也就是说你只要用这个 handler 的sendMessage、post消息天生就带“特殊通行证”。这种用法在系统源码里非常普遍。像 Choreographer 内部创建 Handler 时就直接传了true。因为 Choreographer 的帧回调必须保证自身节奏不能被其他同步消息干扰。3.2 单条 Message 设置异步如果你不想新建一个 Handler而是只想让某一条消息异步化可以直接设置 MessageMessage msg Message.obtain(); msg.what MSG_DO_FRAME; msg.setAsynchronous(true); asyncHandler.sendMessageAtTime(msg, frameTime);setAsynchronous(true)会在 Message 的 flags 里置上FLAG_ASYNCHRONOUS。只要在入队之前设好next()里的isAsynchronous()就能识别到它。这里要提醒一句Message.setAsynchronous()的生效时机是“入队前”。如果你先调用了sendMessage再回去设置已经来不及因为对象已经进入链表。实际开发里我见过有人把设置写在handleMessage里那完全起不到作用。3.3 到底什么条件能越过屏障把前面内容串起来可以归纳成一张速查表条件结果消息在屏障之前插入不受影响正常执行屏障之后插入的同步消息被屏障卡住必须等屏障移除屏障之后插入且when已到的异步消息可以跳过屏障执行屏障之后插入但when在未来需要等到时间到点不能立刻执行队列中存在多个屏障每个屏障都会拦住后续同步消息直到被各自移除最后一点特别重要。我见过一个系统场景某个页面为了同时调度绘制和输入插入了两个屏障结果其中一个 token 没移除导致整个队列的同步消息被双重拦截画面直接冻结。多屏障场景下移除顺序必须严格保证每个 token 都不漏。4. 系统框架里的实际应用场景4.1 Choreographer 的非异步帧调度Choreographer 是 Android 排帧的核心角色它把每一帧的工作分成输入、动画、遍历等阶段。为了保证自己的调度消息不被普通同步消息挤到后面Choreographer 内部的 Handler 是异步的private void scheduleFrameLocked(long now) { if (!mFrameScheduled) { mFrameScheduled true; Message msg mHandler.obtainMessage(MSG_DO_FRAME); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, now); } }MSG_DO_FRAME是异步消息所以即便主线程队列前面有同步屏障Choreographer 的帧消息也能穿过去。这样做的好处是帧回调能稳定地按 VSYNC 节奏触发而不是看普通消息队列的脸色。但要注意Choreographer 本身并不负责插入同步屏障。屏障通常由更上层的 ViewRootImpl 在安排遍历时插入。4.2 ViewRootImpl 里的经典用法真正把“插入屏障 异步回调”串起来的是ViewRootImpl.scheduleTraversals()void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled true; mTraversalBarrier mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); notifyRendererFramePending(); } }这段代码做了三件事在主线程消息队列里插入一个同步屏障记下 token 到mTraversalBarrier向 Choreographer 注册一个 traversal 回调这个回调最终会通过异步消息触发把mTraversalScheduled置为 true防止同一帧里重复安排。为什么要插屏障因为mTraversalRunnable真正执行时会调用performTraversals()做 measure、layout、draw。如果在这个时刻之前其他同步消息比如用户触摸事件、View.invalidate()引发的消息、定时器消息先插入队列它们就可能排在 traversal 前面执行导致绘制被推到下一帧。屏障一插后续来的同步消息全部挡在门外只有 traversal 的异步回调能通过从而保证“这一帧的遍历”尽可能连续执行。等到真正开始执行遍历时doTraversal()里会移除屏障void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); ... performTraversals(); } }这里mTraversalScheduled不仅是个标志位它还同时承担了“防重入”和“避免重复移除屏障”的作用。如果 doTraversal 还没跑完又来个 invalidate新调用发现mTraversalScheduled true就不再重新插屏障也就不会产生两个屏障叠在一起的问题。这套设计非常巧妙值得在自己做调度器时借鉴。4.3 自定义场景里的屏障与异步组合同步屏障不只在 View 体系里出现。凡是需要“通知某条线程优先处理一组任务同时挡住其他普通任务”的场景都可以用这个组合。比如某个跨平台渲染项目里我需要在子线程维护一个渲染循环同时又要处理资源配置消息。普通配置消息可以排队慢慢执行但渲染帧提交必须尽快。我当时用 HandlerThread 建了一个子线程队列然后通过反射调用postSyncBarrier再用一个 async Handler 发渲染指令。效果是渲染指令永远优先配置消息在屏障存在期间全部等待渲染空闲时再移屏障放行。这里也提醒一句postSyncBarrier和removeSyncBarrier在标准 SDK 里是隐藏 API普通应用直接调用会编译失败。想用只能走反射但反射有版本兼容和系统安全性风险。如果只是在应用层做常规开发不建议在生产代码里这么干了解原理、能读懂系统源码就够了。系统开发或自研 OS 时才需要真正动手实现这类调度。5. 实战中的坑与排查记录5.1 屏障忘了移除队列直接堵死这是同步屏障最经典的翻车现场。有一次我在调试一个自研渲染流程时为了确保某批异步任务优先在主线程队列插了屏障。结果流程中某个异常分支直接提前 return把removeSyncBarrier跳过了。现象非常诡异页面不刷新了触摸点击也没反应但线程没崩dumpsys看 CPU 占用还是正常的。原因就是屏障一直留在队列里后续所有同步消息全部排队等着。UI 线程被nativePollOnce挂起因为屏障后面没有可执行的异步消息。直到我把它移除整条队列才恢复流动。吃一堑长一智现在我做这类调度一定会把移除操作放进try/finally里int token queue.postSyncBarrier(); try { // 执行关键异步任务 } finally { queue.removeSyncBarrier(token); }finally保证无论如何都能拆墙哪怕中间抛异常也不会把队列永远卡死。这个习惯在系统开发里尤其重要。5.2 异步消息设了却依然不执行还有一次某同事反馈我明明用 async Handler 发了一条消息为什么屏障存在时它还是没跑查下来发现async Handler 是在主线程创建的但发消息时sendMessageAtTime的时间戳被设成了当前时间加上 500ms。屏障虽然放行异步消息但它依然要遵守消息链表的时间排序。next()在处理异步消息前会先判断now msg.when如果时间还没到就继续睡到时间点。所以“异步”只解决优先于同步消息的问题不提前于“时间点”执行。排查这类问题先看when再看flag顺序对了问题基本就找出来了。5.3 怎么确认当前队列里有没有屏障我常用的方法有两种。第一种是在自研工具里直接 dump 主线程消息队列。MessageQueue在 API 28 之后提供了dump方法PrintWriter pw new PrintWriter(new FileOutputStream(dumpFile)); Looper.getMainLooper().getQueue().dump(pw, ); pw.flush();dump 输出里能看到队列里每个 Message 的target、when、arg1。如果发现某个消息target null且arg1有值那大概率就是屏障。再往下翻如果它后面跟了一大串同步消息基本可以断定屏障泄漏了。第二种方式是在 Android Studio 里抓 Java Heap。把MessageQueue实例展开看mMessages链表中的节点同样可以找到target null的特殊节点。这个方法适合在 UI 卡死现场抓证据配合线程栈一起看定位很快。我个人的经验是屏障泄漏导致的问题最明显的特点是“主线程没有死循环也没有异常就是事件不响应”。遇到这种症状第一反应就该看看队列里有没有残留的 barrier token而不是盲目去查业务代码。5.4 关于屏障和消息回收的注意点屏障消息使用Message.obtain()获取移除时调用recycleUnchecked()回收到消息池。如果在插入屏障后、移除前有人误把这个消息池里的对象重新取出来用会破坏队列结构。系统代码里通过markInUse()和isInUse()做了保护但自己通过反射调用时这些机制可能没被正确约束操作要格外小心。另外removeSyncBarrier传错 token 会直接抛IllegalStateException。尤其是在多屏、多窗口环境下同一线程可能有多个屏障共存token 串了就会出问题。设计时可以把 token 存在一个强引用字段上像 ViewRootImpl 的mTraversalBarrier那样确保移除和插入严格配对。说实话同步屏障这套设计本身不复杂它就是消息队列里一个特殊的“闸门”节点。真正复杂的是它和异步消息、时间排序、队列唤醒这些机制组合起来后的行为边界。读源码时你可能会觉得逻辑很简单到线上问题里走一遭才会发现有多少细节能坑人。如果这系列还有下一篇我觉得可以接着聊 Choreographer 的帧回调完整流程或者把 ViewRootImpl 的遍历调度整体拉通。只要记住了“屏障挡同步、放异步”这个核心后面理解这些系统设计都会顺手很多。