View 系统把界面变化攒到帧上处理。invalidate() 或 requestLayout() 之后,ViewRootImpl 把 Traversal 安排到后续帧,同时往主线程的 MessageQueue 插入一道同步屏障。VSync 信号到达时,Choreographer 将doFrame()包装成一条异步消息,越过屏障后面排队的同步消息。
完整链路:
VSync:提供帧调度时机
VSync 把应用渲染、SurfaceFlinger 合成和屏幕刷新对齐到同一套节奏上。Choreographer 接收面向应用的 VSync 信号,在对应帧里统一调度输入、动画、布局和绘制。
它给的是一帧的起跑信号,不是新画面已经上屏。主线程遍历、渲染执行、Buffer 提交都得在这一帧的时间预算内做完,后面还排着 SurfaceFlinger 合成和显示器呈现。
VSync 也不会每来一次就跑一遍完整的 View 遍历。只有已经安排了帧回调,相关工作才会在后续 VSync 中执行;同一帧到来前的多次刷新请求会合并成一次 Traversal。
同步屏障:改变 MessageQueue 的取消息规则
MessageQueue 里的普通 Message 按 when 排列,target 指向处理它的 Handler,默认都是同步消息。
同步屏障是一条 target == null 的特殊 Message。它同样按 when 插入队列,但没有 Handler 可以分发。屏障到达队首后,MessageQueue.next() 跳过它后面的同步消息,去找第一条已到执行时间的异步消息。
text
屏障生效前
同步 A -> 同步 B -> 异步 C
按 when 和队列顺序取消息
屏障到达队首后
[屏障] -> 同步 A -> 同步 B -> 异步 C
└-> 优先取出已到期的异步 C
屏障管不到排在它前面的消息,那些照旧按顺序执行;只有屏障成为队首,它后面的同步消息才会被压住。
屏障的 when 只决定它在队列中的位置,不是过期时间。屏障成为队首后,即使它和后续同步消息的 when 都已经过去,这些消息仍然不会被取出。只有移除屏障后,已到期的同步消息才会恢复分发;屏障持续越久,它们的等待时间就越长。
异步消息能越过屏障,但也只是获得被取出的资格:when 没到照样等,正在执行的主线程任务照样不打断。没有屏障的时候,异步标记不会带来任何优先级。
ViewRootImpl 如何安排 Traversal
invalidate()、requestLayout() 这些操作最终都会走到 ViewRootImpl.scheduleTraversals(),安排一次 Traversal。核心就两行:
java
barrierToken = queue.postSyncBarrier();
choreographer.postCallback(CALLBACK_TRAVERSAL, traversalRunnable, null);
postSyncBarrier() 返回一个 token,留着后面移除屏障用。postCallback() 把 traversalRunnable 放进 Choreographer 的 Traversal 回调队列,并请求后续 VSync。
这两行动的是两个不同的队列,容易混起来:屏障在主线程的 MessageQueue 里,traversalRunnable 在 Choreographer 内部的回调队列里。所以 traversalRunnable 并不是一条被屏障压住的同步 Message,它由 Choreographer 在 doFrame() 的 Traversal 阶段直接调用。
mTraversalScheduled 负责去重。当前 Traversal 还没执行,后续刷新请求就不会再插一道屏障或重复注册回调。
VSync 如何越过屏障
VSync 到来时,Choreographer.FrameDisplayEventReceiver 不在接收回调里直接做完整帧的活。它把 VSync 事件包成一条主线程消息,标记为异步:
java
Message message = Message.obtain(handler, frameReceiver);
message.setAsynchronous(true);
这条异步消息的 Runnable 最终调用 doFrame()。屏障此时已在队首,它就能越过后面那些同步消息,帧回调不必再等它们。
doFrame() 按固定阶段处理已到期的帧回调。当前 AOSP 主分支的顺序是:
text
Input
-> Animation
-> Insets Animation
-> Traversal
-> Commit
Traversal 阶段执行 ViewRootImpl 注册的 traversalRunnable,落到 doTraversal()。它先拿之前保存的 token 移除屏障,再进 performTraversals():
java
queue.removeSyncBarrier(barrierToken);
performTraversals();
屏障移除得早,但队列不会立刻放行------这条 VSync 异步消息还在执行中。等 performTraversals() 和 doFrame() 里剩下的回调都走完,Looper 才会去取下一条消息。
一帧中的队列变化
把上面几段串起来看一遍。假设主线程已有一条待执行消息 A,界面更新安排了 Traversal,之后又来了同步消息 B:
text
1. scheduleTraversals() 执行后
同步 A -> [屏障] -> 同步 B
2. 同步 A 执行完,屏障到达队首
[屏障] -> 同步 B
MessageQueue 暂不返回 B
3. VSync 到来,异步帧消息入队
[屏障] -> 同步 B -> 异步 Frame
└-> 取出异步 Frame
4. doFrame() 进入 Traversal
移除屏障 -> performTraversals()
5. 当前帧回调结束
Looper 恢复取出同步 B
A 的顺序没被打乱,B 被压到本帧 Traversal 开始之后------屏障要的就是这个效果。
机制边界
同步屏障调整的是消息的可分发性,不是线程抢占优先级。这条界线划出几个边界:
- 屏障不中断正在执行的主线程任务。当前任务跑得久,VSync 异步消息一样得等。
- 屏障之前的消息照序执行,不会被跳过。
- 通道不是给
doFrame()独享的,其他已到期的异步消息也能越过屏障。 - 异步消息只让帧处理拿到执行机会,整帧能不能在截止时间前做完是另一回事。
postSyncBarrier()和removeSyncBarrier()是 Framework 内部接口,必须成对使用。屏障忘了移除,后面的同步消息就一直卡着。
所以屏障解决的问题很具体:不让后来的普通同步消息插在已安排的 Traversal 前面。主线程当前任务过长、Traversal 本身过重、渲染执行超时,这些它都管不了。
总结
两样东西,两件事:VSync 给出与显示刷新对齐的调度时机,同步屏障压住屏障后的同步消息、让 VSync 异步消息得以进入 doFrame()。
分工也就清楚了:ViewRootImpl 安排 Traversal 时插屏障,Choreographer 在 VSync 到来后用异步消息执行帧回调,Traversal 一开始就把屏障撤掉。已经安排好的 View 遍历因此不会被后来的普通同步消息一再推迟。