VSync信号与Choreographer的关系

说到Android的流畅度,绕不开一对黄金搭档——VSync信号和Choreographer。说白了,整个画面渲染的“心跳”就是由它们俩控制的。我刚开始接触性能优化时,以为帧率波动只是代码写得太烂,后来才明白,不懂VSync和Choreographer,你连问题出在哪都找不准。

一句话总结:VSync是硬件的“发令枪”,Choreographer是软件的“调度员”。发令枪一响,调度员就开始安排干活。

4.1 VSync信号基础

VSync,全称Vertical Synchronization(垂直同步)。硬件层面,屏幕刷新率是固定的——比如60Hz的屏幕,每16.6ms刷新一次。VSync信号就是屏幕在每次刷新前发出的一个脉冲,告诉系统:“我要开始画下一帧了,你准备好了吗?”

如果没有VSync,系统可能在屏幕刷新到一半时提交新数据,画面就会出现撕裂(tearing)——你想想看,一半是上一帧,一半是下一帧,那画面简直不能看。

帧率与帧时间的关系:

屏幕刷新率 帧时间 VSync周期
60 Hz 16.6 ms 16.6 ms
90 Hz 11.1 ms 11.1 ms
120 Hz 8.3 ms 8.3 ms

我做过一个项目,设备是90Hz屏,但掉帧频发。查了半天才发现——业务方在锁屏亮起的瞬间,往主线程丢了一个巨耗时的任务,单次帧时间飙到40ms。那是90Hz的设备啊,每一帧只有11.1ms的时间,40ms相当于掉了3帧多。嗯,这里要注意:帧时间超过VSync周期,就是掉帧的开始。

4.2 Choreographer调度机制

Choreographer,直译过来是“舞蹈编排师”。我个人觉得这个名字起得特别形象——它负责把输入事件、动画、布局、绘制这些动作,按VSync的节奏编排好,让它们有条不紊地执行。

核心工作流程:

  • 监听VSync信号
  • 回调Choreographer.FrameCallback
  • 依次处理输入、动画、遍历、绘制
  • 把结果交给SurfaceFlinger合成

每个FrameCallback里,Choreographer会给你一个frameTimeNanos参数——这是VSync信号发生的时间戳,单位是纳秒。我个人习惯拿它来做精确的帧率统计,比System.currentTimeMillis()准得多。

小技巧:用Choreographer.getInstance().postFrameCallback()来注册回调,可以精确跟踪每一帧的开始时间。

为什么会每次回调都注册?
因为Choreographer默认只回调一次。如果你需要持续监听,必须在回调里再次postFrameCallback。这一开始也坑过我——我写了个监控工具,结果只输出了一帧的数据,排查了半天才发现是注册方式不对。

4.3 VSync与Choreographer的配合流程

我来画一张图,帮你看清这整条链路:

VSync与Choreographer调度流程 硬件VSync信号源 屏幕刷新率固定(60/90/120Hz) 发送脉冲 Choreographer 监听VSync + 编排任务 分发帧回调 输入事件 触摸/按键处理 动画处理 ValueAnimator等 遍历 & 绘制 measure / layout / draw SurfaceFlinger合成 组合各图层 → 送显存 屏幕显示 等待下一个VSync刷新

关键点:如果一个周期的所有任务在下一个VSync到来前没完成,画面就会延迟一帧。这就是我们常说的“掉帧”。

4.4 掉帧的根本原因

掉帧,说白了就是帧时间 > VSync周期。具体场景有以下几种:

  • 主线程阻塞:你在主线程做了太多事——比如解析复杂JSON、做磁盘IO、加载大图。Choreographer在等主线程空闲,但主线程一直忙,于是VSync过去了,下一帧才开始处理。
  • 过度绘制:布局嵌套太深,draw方法执行时间远超预期。我曾经接手过一个项目,一个ListItem里有12层嵌套,每次绘制耗时20ms——在60Hz的设备上直接超了3ms。去掉两层后,帧率稳定了。
  • GC抖动:频繁创建对象触发GC,导致主线程暂停。这个在列表快速滑动时特别明显。

注意:掉帧不等于帧率低。有时候帧率看似在60fps,但掉帧次数(jank count)很高。因为掉帧的定义是“连续两帧之间时间间隔超过16.6ms”。我曾经在项目里发现,有些帧跑在15ms,有些跑在18ms,虽然平均是16.5ms,但掉帧次数达到了20次/秒——用户感知是“微卡”。

4.5 通过Choreographer监控掉帧

监控掉帧,我推荐用Choreographer.FrameCallback做自定义统计。直接上代码:

public class JankMonitor {
    private long lastFrameTimeNanos = 0;
    private long vsyncPeriod;
    
    public void start() {
        vsyncPeriod = getVsyncPeriod();
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                if (lastFrameTimeNanos != 0) {
                    long frameInterval = frameTimeNanos - lastFrameTimeNanos;
                    long frameIntervalMs = frameInterval / 1_000_000;
                    
                    if (frameIntervalMs > vsyncPeriod) {
                        int droppedFrames = (int) (frameIntervalMs / vsyncPeriod);
                        Log.e("JankMonitor", "掉帧 " + droppedFrames + " 帧,耗时 " + frameIntervalMs + "ms");
                        // 上报到APM平台
                    }
                }
                lastFrameTimeNanos = frameTimeNanos;
                Choreographer.getInstance().postFrameCallback(this);
            }
        });
    }
    
    private long getVsyncPeriod() {
        return (long) (1000f / 
            DisplayManager.getInstance().getDisplay(0).getRefreshRate());
    }
}

代码做了什么?

  • 获取两次frameTimeNanos的差值
  • 和VSync周期比较
  • 一旦超时,计算掉帧数并上报

我的经验:这个frameIntervalMs > vsyncPeriod的判断有时候会误报——比如页面第一次打开时,因为没有上一帧数据,需要特殊处理。我一般会在第一次回调时把lastFrameTimeNanos赋值,不做掉帧检测。

4.6 VSync与Choreographer的调优策略

明白了它们的配合关系,调优就有方向了:

  1. 减少主线程工作量:把IO、解析、编解码都丢到子线程。Choreographer只管UI,别让它等太久。
  2. 避免在FrameCallback里做重操作:我个人见过有人直接在回调里做位图缩放,帧率直接掉到30fps。回调里只做轻量级统计,重活交给Handler或子线程。
  3. 利用VSync的节奏做预加载:比如列表滑动时,提前加载下一帧需要的数据。我用过Choreographer.postFrameCallback来做隔帧加载,效果不错。

我曾经踩过一个坑:接了一个高刷新率屏项目(120Hz),但Choreographer回调里用了Thread.sleep(5)来做节流。结果就是——每帧多睡了5ms,120Hz的周期是8.3ms,直接掉到60fps以下。后来改成用帧时间戳做精度控制,才把问题解决。结论:绝不要在UI线程里加sleep。

4.7 用Systrace验证VSync与Choreographer

Systrace是最直观的验证工具。抓取trace时关注以下几点:

  • VSync信号线:如果VSync之间间距明显大于16.6ms,说明有掉帧。
  • Choreographer#doFrame:这个阶段耗时如果超过VSync周期,就是主线程瓶颈。
  • SurfaceFlinger#composeSurfaces:这个阶段耗时过长,可能是合成层数太多或GPU过载。

怎么看?

# 抓取15秒的trace
python systrace.py -t 15 -o trace.html gfx view input sched

打开trace.html,在搜索框输入“jank”或“Choreographer”,就能看到掉帧标记。我个人习惯按帧时间排序,先挑出耗时最长的几帧,然后点进去看具体哪个方法占用了主线程。

小结:VSync是硬件的节拍,Choreographer是软件的指挥。理解了这个关系,你就能从系统的角度去分析掉帧,而不仅仅是在代码里瞎猜。