4、SurfaceFlinger与VSync:VSync信号机制、Choreographer工作原理、帧率控制与掉帧检测

说到Android的流畅度,VSync是个绕不开的话题。我刚开始做多媒体开发那会儿,经常遇到界面卡顿、视频播放不流畅的问题。后来深入研究了VSync机制,才明白原来整个Android的渲染流水线都是围绕这个信号来运转的。

说白了,VSync就是屏幕刷新时的同步信号。没有它,GPU和CPU各干各的,画面撕裂、掉帧就是家常便饭。今天我就把这块内容掰开揉碎了讲清楚。

4.1 VSync信号机制

VSync的全称是Vertical Synchronization,也就是垂直同步。它的核心作用只有一个:让CPU和GPU的渲染节奏,跟屏幕的刷新节奏保持一致

屏幕刷新是从左到右、从上到下逐行扫描的。当扫描到最底部时,会有一个短暂的空白期,叫VBlank(垂直消隐期)。VSync信号就在这个时刻产生。

关键点:VSync信号告诉系统——屏幕刚完成一帧的显示,现在可以准备下一帧了。

Android 4.1之后引入了Project Butter(黄油计划),核心就是基于VSync的同步机制。我记得当时第一次看到这个方案时,心里想:这不就是给渲染流程加了个节拍器吗?

硬件层面,VSync由Display硬件产生。软件层面,SurfaceFlinger通过HWComposer接收这个信号,然后分发给各个模块。

4.2 Choreographer工作原理

Choreographer,翻译过来就是「编舞者」。这个名字起得很形象——它负责协调所有跟UI绘制相关的操作,让它们踩着VSync的节拍跳舞。

每个App的主线程都有一个Choreographer实例。当你调用invalidate()请求重绘时,Choreographer并不会立即执行,而是等到下一个VSync信号到来时再统一处理。

它的工作流程大致是这样的:

  1. App注册一个回调(Callback),等待VSync
  2. VSync信号到达,Choreographer被唤醒
  3. 按优先级依次执行:输入事件处理 → 动画计算 → 布局测量 → 绘制
  4. 最终生成一个DisplayList,交给RenderThread去渲染

嗯,这里要注意:Choreographer的回调执行顺序是固定的。我遇到过一个问题,就是动画回调里做了耗时操作,导致布局和绘制被延迟,整个界面就卡住了。

避坑指南:我曾经在动画回调里做网络请求,结果界面掉帧严重。后来把网络请求放到子线程,用Handler.post()切回来更新UI,问题就解决了。

4.3 帧率控制与掉帧检测

帧率控制,说白了就是让App的渲染速度跟屏幕刷新率匹配。60Hz的屏幕,每帧只有16.6ms。如果一帧的渲染时间超过这个阈值,就会发生掉帧。

掉帧的检测方法,我一般用两种:

  • Choreographer.FrameCallback:通过计算两次回调的时间差,判断是否掉帧
  • SurfaceFlinger的dump信息:通过dumpsys SurfaceFlinger查看每帧的提交时间

代码示例:

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    long lastFrameTimeNanos = 0;
    
    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastFrameTimeNanos == 0) {
            lastFrameTimeNanos = frameTimeNanos;
            Choreographer.getInstance().postFrameCallback(this);
            return;
        }
        
        long diff = (frameTimeNanos - lastFrameTimeNanos) / 1000000L;
        if (diff > 16.6f) {
            int droppedFrames = (int)(diff / 16.6f);
            Log.w("FrameMonitor", "掉帧: " + droppedFrames + " 帧, 耗时: " + diff + "ms");
        }
        
        lastFrameTimeNanos = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
});

这个代码我一直在用。你想想看,如果diff是50ms,那就意味着掉了3帧左右。这时候就要去排查是CPU忙、GPU忙,还是主线程被阻塞了。

4.4 SurfaceFlinger的合成与调度

SurfaceFlinger是Android的窗口合成服务。它接收各个App的Buffer(缓冲区),然后合成一帧显示到屏幕上。

它的工作节奏是这样的:

  1. VSync信号到达,SurfaceFlinger被唤醒
  2. 检查所有Layer(图层)是否有新的Buffer提交
  3. 如果有,就进行合成(GPU合成或HWC合成)
  4. 合成完成后,提交到Display显示

这里有个关键点:SurfaceFlinger的合成频率跟屏幕刷新率是同步的。如果App提交Buffer的速度跟不上,SurfaceFlinger就会重复使用上一帧的Buffer,这就造成了掉帧。

注意:SurfaceFlinger的合成是串行的。如果一个App的Buffer提交延迟了,会影响所有App的显示。这就是为什么一个卡顿的App会拖慢整个系统。

4.5 核心流程图

下面这张图是我自己画的,展示了VSync信号从产生到最终显示的完整流程:

VSync信号驱动渲染流程 硬件层:Display产生VSync信号 VBlank期间触发,频率=屏幕刷新率(60Hz/90Hz/120Hz) SurfaceFlinger:接收VSync信号 通过HWComposer获取信号,唤醒合成线程 App主线程:Choreographer 执行输入→动画→布局→绘制 App RenderThread 将DisplayList渲染为GPU指令 提交Buffer到SurfaceFlinger 提交Buffer到SurfaceFlinger 等待下一个VSync

从这张图可以看得很清楚:VSync信号是整个渲染流水线的起点。SurfaceFlinger收到信号后,一方面通知App开始准备下一帧,另一方面开始合成当前帧。这两条线是并行进行的。

4.6 掉帧的常见原因

根据我的经验,掉帧的原因无非就这几类:

原因分类 具体表现 排查方法
主线程阻塞 Choreographer回调执行时间超过16ms Systrace查看主线程耗时
GPU负载过高 渲染指令太多,GPU处理不过来 Profile GPU Rendering工具
Buffer提交延迟 App提交Buffer时已经错过了VSync窗口 dumpsys SurfaceFlinger查看Buffer状态
内存抖动 频繁GC导致主线程暂停 Memory Profiler查看GC频率

我个人习惯用Systrace来定位掉帧问题。打开Systrace,选中Graphics和Input两个标签,跑一遍场景,就能看到每一帧的耗时分布。如果看到Choreographer.doFrame那一块是红色的,那就说明主线程超时了。

小技巧:在Android Studio的Profiler里,可以直接看到掉帧的帧号和时间点。双击掉帧的那一帧,还能看到当时的调用栈,非常方便。

嗯,关于VSync和SurfaceFlinger的内容,今天就先讲到这里。这块知识比较底层,但理解了它,你就能明白为什么有些App会卡、为什么有些动画会跳帧。说白了,整个Android的流畅度,都系在这根VSync信号线上。

公众号:蓝海资料掘金营,微信deep3321