VSync 机制详解:垂直同步信号产生原理、硬件 VSync 与软件 VSync、VSync 在 Android 中的传递路径
讲动画刷新,绕不开 VSync。说白了,VSync 就是整个 Android 显示系统的节拍器。没有它,画面撕裂、掉帧、卡顿全都会找上门来。我刚开始接触显示驱动时,总觉得 VSync 就是个简单的硬件中断,后来踩了不少坑才明白——它远没那么简单。
垂直同步信号是怎么产生的?
先看硬件层面。显示设备(LCD 屏、OLED 屏)刷新画面时,电子枪或扫描电路从左到右、从上到下逐行扫描。扫完一行叫水平同步(HSync),扫完一整帧叫垂直同步(VSync)。
VSync 信号产生的时机,就是扫描线从屏幕右下角回到左上角的那段空白期——垂直消隐区间(VBlank)。
关键点:VBlank 期间屏幕不传输像素数据,这是 CPU/GPU 更新帧缓冲区的安全窗口。如果在非 VBlank 期间更新缓冲区,就会出现画面撕裂(tearing)。
我做过一个项目,硬件平台比较老,VBlank 持续时间只有 1.2ms。当时没注意这个参数,结果每次刷帧都撞在非 VBlank 窗口上,画面撕裂得一塌糊涂。后来查了 datasheet 才发现,VBlank 太短,根本来不及完成一次完整的 buffer swap。
硬件 VSync vs 软件 VSync
这里有个容易混淆的概念:硬件 VSync 和软件 VSync。很多人以为它们是一回事,其实差别很大。
| 对比项 | 硬件 VSync | 软件 VSync |
|---|---|---|
| 信号源 | 显示控制器(Display Controller) | 软件定时器或模拟信号 |
| 精度 | 纳秒级,与硬件时钟同步 | 毫秒级,受调度延迟影响 |
| 功耗 | 低,硬件直接触发中断 | 较高,需要 CPU 轮询或定时器 |
| 适用场景 | Android 原生显示、游戏 | 模拟器、无硬件 VSync 的旧平台 |
| 可靠性 | 高,不受 CPU 负载影响 | 低,高负载下容易漂移 |
硬件 VSync 是真正的「硬中断」。显示控制器在 VBlank 起始时刻拉高一个 GPIO 引脚,触发 SoC 的 IRQ 控制器,然后内核的 VSync 驱动响应中断,向上层投递事件。
软件 VSync 则是用 hrtimer 模拟的。我记得在某个低端平板项目上,硬件不支持 VSync 中断输出,只能用软件方案。结果跑 60fps 动画时,timer 周期被调度延迟拉长到 18ms 甚至 20ms,帧率直接掉到 50fps 以下。后来我硬是给显示控制器打了一个补丁,把 TE(Tearing Effect)引脚复用成 VSync 中断才解决。
避坑指南:我曾经在选型阶段忽略了一个细节——硬件 VSync 中断的优先级。如果中断被设置为普通 IRQ,在高负载下可能被其他中断淹没,导致 VSync 事件丢失。建议将 VSync 中断设置为快速中断(IRQF_NO_SUSPEND | IRQF_EARLY_RESUME),并绑定到独立的 CPU 核心。
VSync 在 Android 中的传递路径
Android 的 VSync 传递路径,从硬件到应用层,一共经过 4 层。每一层都有自己的职责和坑。
第 1 层:显示控制器
硬件 VSync 中断从这里出发。显示控制器在 VBlank 起始时刻拉高中断线。这里有个细节:有些控制器支持「伪 VSync」模式,即用内部定时器模拟 VSync,不依赖外部 TE 信号。我建议你在项目初期就确认清楚——硬件到底支不支持真正的 VSync 中断输出。
第 2 层:内核驱动
中断到达后,驱动要做三件事:
- 读取时间戳——用 ktime_get() 记录精确的 VSync 到达时刻
- 更新状态——标记 VBlank 开始,通知 DRM 核心层
- 唤醒等待队列——向上层投递 VSync 事件
我遇到过一个问题:驱动里用 jiffies 取时间戳,精度只有 10ms 级别。结果 SurfaceFlinger 拿到的 VSync 时间偏差很大,导致动画帧率抖动。后来改成 ktime_get_real_ns() 才解决。
第 3 层:SurfaceFlinger / HWComposer
SurfaceFlinger 通过 /dev/dri/card0 或 /dev/graphics/fb0 读取 VSync 事件。它内部维护一个 VSync 分发器,把事件分发给所有注册的监听器——包括 Choreographer。
这里有个性能陷阱:如果 SurfaceFlinger 主线程被其他任务阻塞,VSync 事件的分发就会延迟。我曾经在日志里看到 VSync 事件到达后 8ms 才被处理,直接导致掉帧。解决方案是把 VSync 分发线程的优先级提到 RT 级别。
第 4 层:Choreographer
应用层的 VSync 接收者。Choreographer 注册一个 FrameCallback,每次 VSync 到来时调用 doFrame()。doFrame() 里做三件事:
- 处理输入事件
- 执行动画
- 触发 View 绘制
如果 doFrame() 执行时间超过 16.67ms(60fps 的一帧时间),就会错过下一个 VSync,造成掉帧。这就是我们常说的「jank」。
调试技巧:用 adb shell dumpsys gfxinfo 可以查看每一帧的 VSync 延迟。重点关注 FrameCompleted 和 VsyncDelay 两个字段。如果 VsyncDelay 持续超过 5ms,说明 VSync 传递路径上有阻塞。
VSync 偏移与相位控制
Android 8.0 之后引入了 VSync 偏移(VSync Offset)机制。简单说,就是让 SurfaceFlinger 和应用层的 VSync 事件错开一个相位,避免两者同时竞争 CPU/GPU 资源。
默认配置下,应用 VSync 比硬件 VSync 提前 2ms,SurfaceFlinger VSync 滞后 4ms。这样应用先完成渲染,SurfaceFlinger 再合成,流水线效率更高。
我在一个双屏项目中调过这个参数。主屏 60Hz,副屏 30Hz,默认偏移导致副屏动画卡顿。后来把副屏的 VSync 偏移调到 8ms,问题解决。调参命令如下:
# 查看当前 VSync 偏移
adb shell dumpsys window | grep -i vsync
# 设置应用 VSync 偏移(单位:ns)
adb shell settings put global vsync_offset_app 2000000
# 设置 SurfaceFlinger VSync 偏移
adb shell settings put global vsync_offset_sf 4000000
注意:VSync 偏移不是越大越好。偏移过大会导致应用和 SurfaceFlinger 之间的流水线空隙变大,反而降低帧率。我建议从 2ms/4ms 开始调,每次增减 1ms,用 systrace 观察帧率变化。
总结一下
VSync 机制是整个动画刷新系统的基石。硬件 VSync 提供精确的节拍,软件 VSync 作为降级方案。从显示控制器到 Choreographer,每一层都可能成为瓶颈。理解这条传递路径,你才能精准定位掉帧问题。