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

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

完整链路:

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 里的普通 Messagewhen 排列,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 里,traversalRunnableChoreographer 内部的回调队列里。所以 traversalRunnable 并不是一条被屏障压住的同步 Message,它由 ChoreographerdoFrame() 的 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 遍历因此不会被后来的普通同步消息一再推迟。

参考资料与源码

相关推荐
mlidongfeng1 小时前
[AI][昇腾950] Scalar 性能优化
人工智能·性能优化
kfzjw0081 小时前
Android 的一帧完整渲染流程:从 VSYNC 到 Screen 的全链路解密
android
乐启国际旅行社有限公司2 小时前
文旅小程序性能优化:分包加载+地图视口懒加载解决景区卡顿与包超限
性能优化·小程序
wWYy.2 小时前
Mysql:覆盖索引
android·数据库·mysql
Kapaseker3 小时前
如果我要从 List 中获取一段元素
android·kotlin
三少爷的鞋3 小时前
一次朋友圈发布流程,理解 Kotlin 协程为什么重新定义异步代码组织方式
android
FungLeo3 小时前
Flutter/Android Release 包连不上网?AndroidManifest INTERNET 权限排查实录
android·flutter
wWYy.5 小时前
Mysql:一行数据是怎么存储的?
android·数据库·mysql
漏刻有时12 小时前
PHP GeoJSON转PNG地图渲染程序开发笔记、源码解读、问题复盘与整改方案
android·笔记·php