2.Handler 里的各种消息
同步屏障和异步消息
其实在 Handler 机制中,有三种消息类型:
- 同步消息。也就是普通的消息。
- 异步消息。通过 setAsynchronous (true) 设置的消息。
- 同步屏障消息。通过 postSyncBarrier 方法添加的消息,特点是 target 为空,也就是没有对应的 handler。也是通过屏障的时间,确定屏障消息插入的位置

这三者之间的关系如何呢?
- 正常情况下,同步消息和异步消息都是正常被处理,也就是根据时间 when 来取消息,处理消息。
- 如果当前消息是一个同步屏障,那么就会从当前消息之后的消息开始寻找,并将找到的下一个异步消息赋给 msg,直到找到异步消息或遍历完整个消息队列。
取消息,next () 方法
1 | |
也就是说同步屏障消息不会被返回,他只是一个标志,一个工具,遇到它就代表要去先行处理异步消息了,提高优先级。
所以同步屏障和异步消息的存在的意义就在于有些消息需要“加急处理”。
同步障碍会阻碍同步消息,只允许通过异步消息
Choreographer
Choreographer 本质上是 Android 的“帧调度器”。
它的作用是:
在每一次屏幕刷新(VSYNC)到来时,统一调度一帧中必须完成的三件事——输入、动画、绘制,保证 UI 和屏幕刷新节奏严格同步,避免掉帧。
它解决的核心问题
手机屏幕是按固定频率刷新的(比如 60Hz / 120Hz),
而 App 的 UI 更新如果不和这个节奏对齐,就会出现:
- 一帧画不完 → 掉帧
- 多次无意义绘制 → 浪费性能
- 动画和触摸不同步 → 卡顿、抖动
Choreographer 就是用来“对齐 App 和屏幕节奏”的。
它是怎么工作的
每次 VSYNC 到来,Choreographer 就会触发一次 UI 帧回调,按顺序执行输入 → 动画 → 绘制,然后把这一帧准时提交给屏幕。
它在一帧里主要干三件事(重点)
处理输入(Input)
- 比如触摸、滑动事件
执行动画(Animation)
- 属性动画、View 动画的进度计算
绘制 UI(Traversal / Draw)
- measure → layout → draw
👉 一帧中只有这三类事情,且顺序是固定的。
- measure → layout → draw
和开发者最直接的关系
- 当你调用
View.invalidate()/requestLayout()时 - 并不会立刻绘制
- 而是告诉 Choreographer:
“下一个 VSYNC 来了记得叫我画一帧”
所以:
UI 更新永远是“等 VSYNC 再统一执行”,而不是随调随画。
为什么会掉帧
在 60Hz 屏幕下,一帧只有 16.6ms:
- 如果你在这一帧里:
- 做了重计算
- 主线程 IO
- 复杂布局
- 导致三件事没在 16.6ms 内完成
👉 Choreographer 就会错过这个 VSYNC,直接掉一帧。
监听掉帧卡顿是否流畅,看两帧之间的时间间隔(即每帧的实际耗时)
1 | |
Handler 包装 dispatchMessage
能帮助定位原因:是因为处理这个消息花了太长时间,导致了卡顿。
Choreographer 调度一帧的流程 👇
- Vsync 信号到来 → Choreographer 收到回调,开始这一帧。
- Input Handling → 处理输入事件(触摸/按键)。
- Animation → 执行动画数值更新。
- Measure/Layout/Draw → 视图树测量、布局、绘制。
- Render → 把绘制结果提交给 GPU。
- Display → SurfaceFlinger 合成并最终显示到屏幕。

同步屏障和异步消息的使用场景
系统把插入屏障和构造异步 Handler 这些东西标记为 @UnsupportedAppUsage,意思就是这些 API 是系统自己用的,不想让开发者调用。那系统是什么时候用的呢?
View 绘制的起点是 ViewRootImpl 的 requestLayout () 开始的,这个方法会去执行上面的三大绘制任务:测量、布局、绘制。调用 requestLayout () 方法之后,并不会马上开始进行绘制任务,而是会给主线程设置一个同步屏幕,并设置 Vsync 信号监听。当 Vsync 信号的到来,会发送一个异步消息到主线程 Handler,执行我们上一步设置的绘制监听任务,并移除同步屏障。
requestLayout 调用 scheduleTraversals
绘制方法 scheduleTraversals,刷新帧率
1 | |
postCallback 方法里执行
1 | |
在该方法中加入了同步屏障,后续加入一个异步消息 MSG_DO_SCHEDULE_CALLBACK,最后会执行到 FrameDisplayEventReceiver,用于申请 VSYNC 信号。
在等待 Vsync 信号的时候主线程什么事都没干,这样的好处是保证在 Vsync 信号到来时,绘制任务可以被及时执行,不会造成界面卡顿。
这样的话,我们发送的普通消息可能会被延迟处理,在 Vsync 信号到了之后,移除屏障,才得以处理普通消息。
同步屏障的移除
在 MessageQueue. java 的 removeSyncBarrier () 方法。
删除屏障消息的方法很简单,就是不断遍历消息队列,知道找到屏障消息,退出循环的条件有两个,一是 p.target == null,说明是屏障消息,二是 p.arg1 == token,也说明 p 是屏障消息,因为在屏障消息入队的时候,设置过 msg. arg1 = token。找到屏障消息后,把它从消息队列中删除并回收。
postSyncBarrier () 和 removeSyncBarrier () 必须成对出现,否则会导致消息队列出现假死情况。
如果不异常同步屏障消息,队列中已经没有了异步消息,但是存在普通消息,所以会一直等异步消息,普通消息得不到执行机会

Surface
在 WMS 中会为这个 Window 分配 Surface,并确定显示层级,可见负责显示界面的是画布 Surface,而不是窗口本身,WMS 将他管理的 Surface 交由 SurfaceFlinger 处理,SurfaceFlinger 将这些 Surface 合并后放入到 buffer 中,屏幕会定时从 buffer 中获取显示数据,显示到屏幕上。
[[6-系统源码#SurfaceView]]
刷新率
在 Android 系统中,屏幕定时从 buffer 中获取显示数据的时间通常是以屏幕刷新率(刷新频率)来衡量的。常见的屏幕刷新率包括60Hz 和120Hz,表示屏幕每秒刷新60次或120次。
具体的刷新时间取决于屏幕刷新率和 VSync 信号的触发。VSync(垂直同步信号)是一个由硬件生成的信号,用于同步屏幕刷新和显示数据的传输。在每个 VSync 信号触发时,GPU会从 buffer 中获取最新的显示数据,并将其显示在屏幕上。
以60Hz 的屏幕刷新率为例,每秒刷新60次,因此每次刷新的时间间隔约为16.67毫秒(1秒/60 = 0.0167秒 ≈ 16.67毫秒)。这意味着,屏幕会大约每16.67毫秒获取一次 buffer 中的显示数据,并更新到屏幕上。
需要注意的是,具体的屏幕刷新率和 VSync 触发时间可能因设备而异。某些设备可能具有不同的刷新率(例如90Hz 或120Hz),因此屏幕获取显示数据的时间间隔也会相应变化。此外,硬件和系统的性能也可能影响刷新时间的准确性。
VSYNC
Android 系统每隔16ms 发出 VSYNC 信号,触发对 UI 进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms 内完成。如果你的某个操作花费时间是24ms,系统在得到 VSYNC 信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms 内看到的会是同一帧画面。
用户容易在 UI 执行动画或者滑动 ListView 的时候感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原因可以导致丢帧,也许是因为你的 layout 太过复杂,无法在16ms 内完成渲染,有可能是因为你的 UI 上有层叠太多的绘制单元,还有可能是因为动画执行的次数过多。这些都会导致 CPU 或者 GPU 负载过重。
vsync 信号是由底层发出的
cpu、gpu功能
- cpu 负责计算 measure layout draw
- gpu 负责渲染 display =>位图
- 每个16ms 会发送一次垂直同步信号 vsync
- 每次信号发送的时候都会从 gpu 的 buffer 中取出渲染好的位图显示在屏幕上,同时如果有需要还会进行下一次的 cpu 计算, 计算好后放入 buffer 中
- 如果计算时间超过了两次 vsync 之间的时间即16ms 则 vsync 信号会把上一次 gpu buffer 中的信息展示出来这时候就是卡顿
- 另外如果页面没有变化屏幕还是一样会去 buffer 中取出上一次的刷新, 只不过 cpu 不再去计算而已