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信号到来时再统一处理。
它的工作流程大致是这样的:
- App注册一个回调(Callback),等待VSync
- VSync信号到达,Choreographer被唤醒
- 按优先级依次执行:输入事件处理 → 动画计算 → 布局测量 → 绘制
- 最终生成一个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(缓冲区),然后合成一帧显示到屏幕上。
它的工作节奏是这样的:
- VSync信号到达,SurfaceFlinger被唤醒
- 检查所有Layer(图层)是否有新的Buffer提交
- 如果有,就进行合成(GPU合成或HWC合成)
- 合成完成后,提交到Display显示
这里有个关键点:SurfaceFlinger的合成频率跟屏幕刷新率是同步的。如果App提交Buffer的速度跟不上,SurfaceFlinger就会重复使用上一帧的Buffer,这就造成了掉帧。
注意:SurfaceFlinger的合成是串行的。如果一个App的Buffer提交延迟了,会影响所有App的显示。这就是为什么一个卡顿的App会拖慢整个系统。
4.5 核心流程图
下面这张图是我自己画的,展示了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