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 层。每一层都有自己的职责和坑。

Android VSync 传递路径 Android VSync 传递路径 显示控制器 (Display Controller) 硬件 VSync 中断产生 VSync 驱动 / DRM/KMS 中断处理 → 向上投递事件 SurfaceFlinger / HWComposer VSync 事件分发 Choreographer / App 回调 doFrame() 开始渲染 每一层都可能成为瓶颈:中断丢失 → 驱动延迟 → 分发抖动 → 应用掉帧

第 1 层:显示控制器

硬件 VSync 中断从这里出发。显示控制器在 VBlank 起始时刻拉高中断线。这里有个细节:有些控制器支持「伪 VSync」模式,即用内部定时器模拟 VSync,不依赖外部 TE 信号。我建议你在项目初期就确认清楚——硬件到底支不支持真正的 VSync 中断输出。

第 2 层:内核驱动

中断到达后,驱动要做三件事:

  1. 读取时间戳——用 ktime_get() 记录精确的 VSync 到达时刻
  2. 更新状态——标记 VBlank 开始,通知 DRM 核心层
  3. 唤醒等待队列——向上层投递 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,每一层都可能成为瓶颈。理解这条传递路径,你才能精准定位掉帧问题。