2.Handler 里的各种消息

同步屏障和异步消息

其实在 Handler 机制中,有三种消息类型:

  • 同步消息。也就是普通的消息。
  • 异步消息。通过 setAsynchronous (true) 设置的消息。
  • 同步屏障消息。通过 postSyncBarrier 方法添加的消息,特点是 target 为空,也就是没有对应的 handler。也是通过屏障的时间,确定屏障消息插入的位置
    Pasted image 20250820142310

这三者之间的关系如何呢?

  • 正常情况下,同步消息和异步消息都是正常被处理,也就是根据时间 when 来取消息,处理消息。
  • 如果当前消息是一个同步屏障,那么就会从当前消息之后的消息开始寻找,并将找到的下一个异步消息赋给 msg,直到找到异步消息或遍历完整个消息队列。

取消息,next () 方法

1
2
3
4
5
6
7
Message msg = mMessages;
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}

也就是说同步屏障消息不会被返回,他只是一个标志,一个工具,遇到它就代表要去先行处理异步消息了,提高优先级。
所以同步屏障和异步消息的存在的意义就在于有些消息需要“加急处理”。
同步障碍会阻碍同步消息,只允许通过异步消息

Choreographer

Choreographer 本质上是 Android 的“帧调度器”。

它的作用是:
在每一次屏幕刷新(VSYNC)到来时,统一调度一帧中必须完成的三件事——输入、动画、绘制,保证 UI 和屏幕刷新节奏严格同步,避免掉帧。

它解决的核心问题
手机屏幕是按固定频率刷新的(比如 60Hz / 120Hz),
而 App 的 UI 更新如果不和这个节奏对齐,就会出现:

  • 一帧画不完 → 掉帧
  • 多次无意义绘制 → 浪费性能
  • 动画和触摸不同步 → 卡顿、抖动

Choreographer 就是用来“对齐 App 和屏幕节奏”的。

它是怎么工作的

每次 VSYNC 到来,Choreographer 就会触发一次 UI 帧回调,按顺序执行输入 → 动画 → 绘制,然后把这一帧准时提交给屏幕。

它在一帧里主要干三件事(重点)

  1. 处理输入(Input)

    • 比如触摸、滑动事件
  2. 执行动画(Animation)

    • 属性动画、View 动画的进度计算
  3. 绘制 UI(Traversal / Draw)

    • measure → layout → draw
      👉 一帧中只有这三类事情,且顺序是固定的。

和开发者最直接的关系

  • 当你调用 View.invalidate() / requestLayout()
  • 并不会立刻绘制
  • 而是告诉 Choreographer:
    “下一个 VSYNC 来了记得叫我画一帧”

所以:

UI 更新永远是“等 VSYNC 再统一执行”,而不是随调随画。

为什么会掉帧
在 60Hz 屏幕下,一帧只有 16.6ms

  • 如果你在这一帧里:
    • 做了重计算
    • 主线程 IO
    • 复杂布局
  • 导致三件事没在 16.6ms 内完成
    👉 Choreographer 就会错过这个 VSYNC,直接掉一帧。

监听掉帧卡顿是否流畅,看两帧之间的时间间隔(即每帧的实际耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FrameMonitor implements Choreographer.FrameCallback {
private long mLastFrameTimeNanos = 0;

@Override
public void doFrame(long frameTimeNanos) {
if (mLastFrameTimeNanos != 0) {
long frameCostMs = (frameTimeNanos - mLastFrameTimeNanos) / 1_000_000;
if (frameCostMs > 16) {
Log.w("FrameMonitor", "一帧耗时: " + frameCostMs + "ms, 已掉帧!");
}
}
mLastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this); // 继续监听下一帧
}
}
// 启动监听: Choreographer.getInstance().postFrameCallback(new FrameMonitor());

Handler 包装 dispatchMessage
能帮助定位原因:是因为处理这个消息花了太长时间,导致了卡顿。

Choreographer 调度一帧的流程 👇

  1. Vsync 信号到来 → Choreographer 收到回调,开始这一帧。
  2. Input Handling → 处理输入事件(触摸/按键)。
  3. Animation → 执行动画数值更新。
  4. Measure/Layout/Draw → 视图树测量、布局、绘制。
  5. Render → 把绘制结果提交给 GPU。
  6. Display → SurfaceFlinger 合成并最终显示到屏幕。
    Pasted image 20250820153519

同步屏障和异步消息的使用场景

系统把插入屏障和构造异步 Handler 这些东西标记为 @UnsupportedAppUsage,意思就是这些 API 是系统自己用的,不想让开发者调用。那系统是什么时候用的呢?

View 绘制的起点是 ViewRootImpl 的 requestLayout () 开始的,这个方法会去执行上面的三大绘制任务:测量、布局、绘制。调用 requestLayout () 方法之后,并不会马上开始进行绘制任务,而是会给主线程设置一个同步屏幕,并设置 Vsync 信号监听。当 Vsync 信号的到来,会发送一个异步消息到主线程 Handler,执行我们上一步设置的绘制监听任务,并移除同步屏障。

requestLayout 调用 scheduleTraversals
绘制方法 scheduleTraversals,刷新帧率

1
2
3
4
5
6
7
8
9
10
11
void scheduleTraversals() {  
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 同步屏障,阻塞所有的同步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//监听Vsync信号,然后发送异步消息 -> 执行绘制任务
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

postCallback 方法里执行

1
2
3
4
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);  
msg.arg1 =callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg,dueTime);

在该方法中加入了同步屏障,后续加入一个异步消息 MSG_DO_SCHEDULE_CALLBACK,最后会执行到 FrameDisplayEventReceiver,用于申请 VSYNC 信号

在等待 Vsync 信号的时候主线程什么事都没干,这样的好处是保证在 Vsync 信号到来时,绘制任务可以被及时执行,不会造成界面卡顿。
这样的话,我们发送的普通消息可能会被延迟处理,在 Vsync 信号到了之后,移除屏障,才得以处理普通消息。

同步屏障的移除
在 MessageQueue. java 的 removeSyncBarrier () 方法。
删除屏障消息的方法很简单,就是不断遍历消息队列,知道找到屏障消息,退出循环的条件有两个,一是 p.target == null,说明是屏障消息,二是 p.arg1 == token,也说明 p 是屏障消息,因为在屏障消息入队的时候,设置过 msg. arg1 = token。找到屏障消息后,把它从消息队列中删除并回收。

postSyncBarrier () 和 removeSyncBarrier () 必须成对出现,否则会导致消息队列出现假死情况。

如果不异常同步屏障消息,队列中已经没有了异步消息,但是存在普通消息,所以会一直等异步消息,普通消息得不到执行机会

deepseek_mermaid_20250905_cc1d26

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 不再去计算而已

2.Handler 里的各种消息
http://peiniwan.github.io/2025/12/bfe220353bca.html
作者
六月的雨
发布于
2025年12月16日
许可协议