VSync 与同步屏障:doFrame() 的优先调度

View 系统把界面变化攒到帧上处理。invalidate() 或 requestLayout() 之后,ViewRootImpl 把 Traversal 安排到后续帧,同时往主线程的 MessageQueue 插入一道同步屏障。VSync 信号到达时,Choreographer 将doFrame()包装成一条异步消息,越过屏障后面排队的同步消息。

完整链路:

flowchart TD A[View 请求刷新或布局] --> B[ViewRootImpl 安排 Traversal] B --> C[插入同步屏障] B --> D[注册 Traversal 回调] D --> E[Choreographer 请求 VSync] E --> F[VSync 到来] F --> G[异步消息进入主线程队列] G --> H[执行 doFrame] H --> I[执行 Traversal 回调] I --> J[移除同步屏障] J --> K[执行 performTraversals]

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 遍历因此不会被后来的普通同步消息一再推迟。

参考资料与源码

相关推荐
Cc.Y11 小时前
Java零基础入门:字符串深度掌握——从基础API到StringBuilder性能优化
java·开发语言·性能优化
打工仔折腾 AI18 小时前
工业场景下时序库与实时计算一体化架构选型实践对比
java·开发语言·后端·python·性能优化·架构·ai agent 实战
余槐i18 小时前
MySQL 深分页从 4 秒到 60 毫秒:延迟关联减少回表的实测与代价
mysql·性能优化·sql优化·执行计划·慢查询优化
无名猿18 小时前
shared_ptr 完全指南:引用计数、控制块与开销
c++·性能优化·内存管理·标准库·现代c++
96245619 小时前
从“一键创建 ECS”到完整恢复状态机:ACK 抢占式节点恢复 Android 工具升级复盘
android
LCD液晶屏1 天前
京瓷液晶屏停产替代:杭州立煌科技工业 LCD 解决方案
android·科技
2501_932750261 天前
Android 自定义 View 入门
android
Zelman1 天前
网络协议性能调优手册
网络协议·性能优化
事圆则缓1 天前
Android 开启线程的几种方式:Thread、线程池、HandlerThread 与协程
android