4、Vsync信号与调度:硬件Vsync与软件Vsync、Choreographer的调度周期、FrameDisplayEventReceiver

Vsync 这个词,做 Android 性能优化的人几乎天天挂嘴边。但说实话,很多人对它的理解就停留在「屏幕每 16ms 刷新一次」这个层面。实际调优的时候,你会发现事情没那么简单。

我记得早年排查一个游戏掉帧问题,明明逻辑帧跑满了 60fps,画面却还是一抖一抖的。后来追到 Vsyntrace 里一看——Vsync 信号根本没对齐。从那天起,我就把 Vsync 的整个链路彻底啃了一遍。

这一章,我们就从硬件到软件,把 Vsync 的来龙去脉拆清楚。

4.1 硬件Vsync:屏幕的原始心跳

硬件 Vsync(HW Vsync)由显示控制器直接产生。屏幕从上到下逐行扫描,扫完一帧后回到顶部,这个瞬间就会触发一个硬件中断——这就是 Vsync 脉冲。

  • 频率固定:60Hz、90Hz、120Hz,取决于屏幕面板
  • 精度极高:纳秒级抖动,几乎不受软件负载影响
  • 功耗敏感:每次中断都会唤醒 CPU,频繁唤醒会耗电

在 Android 早期版本(4.1 之前),应用层直接依赖 HW Vsync 中断。但这么做有个问题——一旦系统负载高了,中断响应延迟就会拖垮帧率。

关键认知:硬件 Vsync 是的「时钟源」,但它太硬了,软件层需要做一层缓冲和分发。这就是「软件 Vsync」出现的理由。

4.2 软件Vsync:DispSync 与双路信号

从 Android 4.1(Jelly Bean)开始,系统引入了 DispSync 模块。它不再直接使用 HW Vsync 中断,而是通过锁相环算法,根据 HW Vsync 的节奏「合成」出一路软件 Vsync 信号。

4.2.1 为什么需要软件Vsync?

  1. 去抖动:硬件中断偶尔会有毛刺,DispSync 可以平滑掉
  2. 相位调整:可以给 App 和 SurfaceFlinger 分配不同的相位偏移,让两者错开执行
  3. 省电:减少不必要的 CPU 唤醒,只在真正需要时派发信号

4.2.2 VSYNC-app 与 VSYNC-sf

DispSync 会生成两路信号:

信号名称目标作用相位偏移
VSYNC-appApp 主线程触发 Choreographer 开始处理输入、动画、绘制通常为 0
VSYNC-sfSurfaceFlinger触发合成与刷新显示通常偏移 2~3ms

这两路信号错开,是为了保证 App 正在绘制的时候,SurfaceFlinger 不会抢屏幕缓冲区。我早年调一个短视频 App 的卡顿问题,就是发现 VSYNC-sf 的相位偏移设得太小,导致 App 还没画完 SF 就开始合成了——结果每一帧都撕裂加掉帧。

4.3 Choreographer的调度周期

Choreographer 的中文意思是「编舞者」。它的工作就是:等 Vsync 信号来了,按顺序执行三件事——

  1. 输入处理(Input)
  2. 动画更新(Animation)
  3. 视图绘制(Traversal)

这三步合起来就是一次 doFrame 调用。你想想看,如果 doFrame 耗时超过 16ms,下一次 Vsync 来的时候上一帧还没处理完,那就会跳过一帧——这就是掉帧。

个人习惯:我一般会在 Choreographer 的 FrameCallback 里打时间戳,用来计算真实的帧间隔。代码很简单,但非常实用。

// 注册 FrameCallback 监控帧率
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    private long lastFrameTime = System.nanoTime();

    @Override
    public void doFrame(long frameTimeNanos) {
        long now = System.nanoTime();
        long interval = now - lastFrameTime;
        lastFrameTime = now;

        // 16.67ms = 16666666ns
        if (interval > 16666666L) {
            int skipped = (int) (interval / 16666666L) - 1;
            Log.w("FrameMonitor", "掉帧 " + skipped + " 帧, 间隔 " +
                    String.format("%.2f", interval / 1e6f) + " ms");
        }

        // 持续注册,形成循环
        Choreographer.getInstance().postFrameCallback(this);
    }
});

这段代码我直接用在了好几个线上项目的监控 SDK 里。你可能会问:frameTimeNanos 和 System.nanoTime() 有啥区别?frameTimeNanos 是 Choreographer 从 Vsync 事件里拿到的硬件时间戳,比 nanoTime 更精确地反映了屏幕的节奏。我建议你用前者来做掉帧计算。

4.4 FrameDisplayEventReceiver:Vsync 的快递员

Vsync 信号从 DispSync 出来,怎么传到 Choreographer 的?中间隔了一个关键的内部类——FrameDisplayEventReceiver。

它继承自 DisplayEventReceiver,本质是一个 Native 层的 Socket 监听器。简单说:

  1. Native 层收到 Vsync 事件后,通过 JNI 回调到 Java 层的 onVsync() 方法
  2. onVsync() 内部会向主线程 Handler 发送一个 Message
  3. Handler 处理 Message 时,调用 Choreographer 的 doFrame()

听起来链路挺长?其实很快。但有一个坑我要特别提醒:

我曾经在线上项目里遇到过一个诡异问题:某些机型上,onVsync 回调里居然有人插入了耗时操作(比如直接写文件)。结果 Vsync 事件被阻塞,Choreographer 迟迟收不到信号,帧率直接腰斩。排查了整整两天才发现。

所以我的建议是:永远不要在 onVsync 或 doFrame 里做任何 IO、锁竞争、大对象分配。 这个方法的使命只有一个——快速分发,然后走人。

4.4.1 核心流程源码示意

以下是我简化后的调用链路伪代码,帮你建立直观印象:

// Native 层收到 Vsync 中断
DisplayEventReceiver::onVsync(timestamp, displayId, frameNumber)

// JNI 调用到 Java
→ FrameDisplayEventReceiver.onVsync(timestamp, ...)

// 向主线程 Handler 发送消息
→ Message msg = Message.obtain(handler, MSG_DO_FRAME, ...)
  handler.sendMessageAtTime(msg, timestamp / TimeUnit.NANOSECONDS.toMillis(...))

// Handler 处理消息
→ Choreographer.doFrame(frameTimeNanos, ...)
    → doCallbacks(INPUT, frameTimeNanos)
    → doCallbacks(ANIMATION, frameTimeNanos)
    → doCallbacks(TRAVERSAL, frameTimeNanos)

4.5 避坑指南与实战要点

结合我自己的排坑经验,给你几个干活用得上的原则:

  • 区分 HW Vsync 和 SW Vsync 的抖动:如果掉帧是随机、不可复现的,多半是软件层抖动;如果是规律性掉帧,检查 VSYNC-sf 相位偏移
  • 不要直接用 HW Vsync 做帧率统计:我从 SysTrace 里拿过数据,HW Vsync 偶尔会丢脉冲,用软件 Vsync 的时间戳更可靠
  • Choreographer 的 doFrame 超时 = 掉帧:超过 16ms 还没返回,下一帧就被跳过了,这是最直接的判断标准
  • FrameDisplayEventReceiver 的回调优先级很高:但它只在主线程执行,所以不要做任何阻塞操作

嗯,说到这我想起一个经典场景。有一次我负责优化一款资讯类 App 的列表滑动帧率。从 Trace 上看,doFrame 每次都在 12~15ms 之间,没超 16ms,但用户就是觉得「不跟手」。后来我用 FrameCallback 算了一下实际的帧间隔——发现每 3~4 帧就会有一个 33ms 的间隔,等于掉了一帧。原因就是 VSYNC-app 的相位偏移和页面 measure 耗时产生了共振。最后通过调整布局层级和减少 measure 次数才解决。

所以你看,光看平均帧率是不够的。你要能看见「每一帧」的间隔,才能找到真正的瓶颈。

我建议:在你项目里试着跑一下上面那段 FrameCallback 代码,把掉帧日志打印出来。哪怕只看 5 分钟,你都会对 App 的帧率表现有全新的认识。

Vsync 信号调度与 Choreographer 工作链路 【硬件层】 【系统服务层】 【应用框架层】 硬件 Vsync 源 (显示控制器) DispSync (软件 Vsync 生成器) VSYNC-app / VSYNC-sf FrameDisplayEventReceiver (接收 Vsync 事件 → 发消息到主线程) Choreographer (doFrame 调度周期) 回调执行:Input → Animation → Draw 💡 核心要点 • HW Vsync 是硬件中断源 • DispSync 合成两路 SW Vsync • FrameDisplayEventReceiver 桥接 • Choreographer 编排三阶段回调 ⏱ 调度周期 • 60Hz → 16.67ms / 帧 • doFrame 超时 → 直接掉帧