使用Vsync信号的由来
显示屏上的内容,其实是从硬件的帧缓冲区(FrameBuffer)中逐行读取的。大致过程如下:
- 从Buffer的起始地址开始,自上而下、从左到右扫描,将内容绘制到屏幕。
- 这个“图像内容”就来自于App端写入的数据。
缓冲区读写冲突与屏幕撕裂
如果对同一个缓冲区同时进行读写,就会出现“屏幕撕裂”(Tearing):
- 即同一时刻,屏幕上方是上一帧内容,下方是新帧内容,画面割裂、不连贯。
解决思路:前后缓冲区
为了解决这个问题,引入了**前缓冲区(FrontBuffer)和后缓冲区(BackBuffer)**的概念:
前缓冲区提供当前帧的显示,后缓冲区负责下一帧的渲染。某个时刻二者交换角色,实现无缝切换和显示。
屏幕刷新率与系统帧率
- 屏幕刷新率(Hz) :每秒刷新屏幕的次数(一般60Hz=16.67ms/帧)
- 系统帧速率(FPS) :每秒应用生成帧的次数
两种极端情况
- 屏幕刷新快于系统绘制:下一帧没准备好,只能重复上一帧内容——画面“卡顿”。
- 系统绘制快于屏幕刷新:还没等前帧显示完,后帧已就绪,前后混合显示——画面“撕裂”。
根本原因: 两者速率不一致,缺乏统一“调度”信号。
供需失衡的本质
本质上就是生产者-消费者模型失衡。必须有机制保证系统合成帧和屏幕刷新同步。
于是——Vsync信号诞生:每到一次Vsync信号(Vertical Synchronization,垂直同步),应用才开始合成和显示新一帧。
Android上的Vsync
Android系统引入Vsync同步,来协调Display(屏幕) 、CPU(合成/生成图片) 、 GPU(绘制/光栅化) 等各方工作。
- Display:显示屏
- CPU:计算像素、生成位图
- GPU:图形加速、栅格化
- DoubleBuffer:缓冲区回收利用,性能优化
绘制超时与卡顿
如果合成一帧超出Vsync间隔,下一帧会延迟到下一个Vsync才显示,造成“卡顿”。
三缓冲(Triple Buffering)
一、双缓冲存在的问题
假设只有两个 buffer(前缓冲、后缓冲):
- CPU:正在绘制/准备一个 buffer(比如 B)。
- GPU:还在渲染上一个 buffer(比如 A)。
- SurfaceFlinger(SF) :需要拿一个“已经完成”的 buffer 合成显示。
但此时两个 buffer 都“占用中”:
- A 还没被 SF 合成完,不能释放;
- B 还在 GPU 渲染,SF用不了;
- 没有 buffer 是 Free 状态,CPU 只能等,SF 也可能等。
这样一来,CPU 只能卡住,等 GPU/SF 完成,会出现 pipeline 阻塞,不能最大限度利用硬件。
二、三缓冲的解决办法
引入第三个 buffer 后:
- 当 SF、GPU、CPU 各自占用一个 buffer 时,还有第三个 buffer 可用,不会卡死 pipeline。
- CPU 可以继续绘制下一个 buffer,不用等 GPU/SF 用完之前的 buffer。
这样各环节流水线并行推进,充分利用每一帧的时间片,不会互相等待,提升流畅度、降低卡顿概率。
三、小结一句话
三缓冲机制,就是为了解决“双缓冲下因同步等待导致的 pipeline 空转/卡死”问题,让 CPU、GPU、SF 能最大程度并行工作,各自有 buffer 可用。
现实中也确实会出现这种场景:
- CPU、GPU、SF 分别操作不同 buffer,如果只有两块 buffer,极易被占满,没有空闲 buffer,pipeline 阻塞。
- 三缓冲让这种等待极大减少,理论上 pipeline 总有一个 buffer 可用,不卡死。
三缓冲对Jank的影响
即使部分帧合成超时(理论上会jank),三缓冲可以减少真正的“丢帧”几率,视觉停留更平滑。
SurfaceFlinger(SF)合成机制
举个例子:
- **每个界面窗口(Window)**对应SurfaceFlinger里的一个Surface对象。
- App端把内容渲染到自己的Surface。
- SF负责将所有Surface进行合成(Compose) ,再输出到屏幕。
Surface & BufferQueue
- 一个Window <=> 一个Surface <=> 一个BufferQueue
Surface不是直接持有BufferQueue,必须通过“生产者”获取。
Surface内部通过BufferQueue(缓冲队列)形成App与SurfaceFlinger的生产者-消费者关系。
- 生产者(Producer) :App端
- 消费者(Consumer) :SurfaceFlinger
Buffer状态变迁
每个Buffer会经历如下四个状态:
- Free:空闲,可被App端使用
- Dequeued:App端获取到,正在渲染
- Queued:App端渲染完成,等待SF合成
- Acquired:被SF获取进行合成
- Free:SF合成完毕,回到空闲
生命周期:free → dequeued → queued → acquired → free
BufferQueue的Slot数目默认64。
未使用(Unused Slot)与已用(Use slot/unactive/active)状态区分。
- App端Surface通过Producer从BufferQueue取“FreeSlot”缓冲区渲染数据;
- 渲染后通过Producer返回队列,等待SF消费。
整体合成架构:
- View的draw方法进行dequeue,写入数据,queue后SF合成。
- 之所以设计成“生产者-消费者”,是为了解耦安全,App无法直接访问系统合成资源。
正常情况下,Queue只应有1个Buffer。如果出现多个,说明“卡顿”。
SurfaceView
SurfaceView就是一个有独立画布的View,相当于“在Window上挖一个洞”,给SurfaceView直接显示内容。
与普通View相比,SurfaceView可以在子线程绘制,也能手动控制帧率。
使用方法
public class GameUI extends SurfaceView implements SurfaceHolder.Callback {
private SurfaceHolder holder;
private RenderThread renderThread;
private boolean isDraw = false;
public GameUI(Context context) {
super(context);
}
public GameUI(Context context, AttributeSet attrs) {
super(context, attrs);
holder = this.getHolder();
holder.addCallback(this);
renderThread = new RenderThread();
}
public GameUI(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {}
/** 绘制线程 */
private class RenderThread extends Thread {
@Override
public void run() {
while (isDraw) {
drawUI();
}
super.run();
}
}
private void drawUI() {
Canvas canvas = holder.lockCanvas(); // dequeue
try {
drawCanvas();
} catch (Exception e) {
} finally {
holder.unlockCanvasAndPost(canvas); // queue
}
}
private void drawCanvas() {}
}
- 普通View必须放在Window中、只能主线程绘制。
- SurfaceView可以任意线程绘制,适合高帧率、频繁刷新的场景。
requestLayout如何向SF申请Surface?
学后检测
一、单选题(每题1分)
-
下列关于Vsync信号的描述,正确的是?
- A. Vsync信号只在GPU和显示屏之间同步
- B. Vsync信号的目的是让App帧生成速度快于屏幕刷新速度
- C. Vsync信号的本质是让系统帧速率与屏幕刷新率保持一致
- D. Vsync信号只在系统出现卡顿时才会触发
答案:C
解析:Vsync的目的是让系统生成帧与屏幕刷新同步,避免撕裂和卡顿;A、B、D都错误或片面。 -
下列哪种情况最容易导致屏幕“撕裂”?
- A. 系统帧速率远小于屏幕刷新率
- B. 屏幕刷新率远大于系统帧速率
- C. 屏幕刷新率和系统帧速率一样
- D. 系统帧速率大于屏幕刷新率
答案:D
解析:系统帧生成太快,屏幕来不及刷新就被新帧覆盖,会出现“撕裂”现象。 -
SurfaceFlinger的主要作用是什么?
- A. 直接渲染App的UI到屏幕
- B. 合成各个App窗口的图像并最终输出到屏幕
- C. 负责内存分配和垃圾回收
- D. 管理SurfaceView的生命周期
答案:B
解析:SF负责把所有窗口(Surface)合成,并最终显示到屏幕上。 -
关于Android中的“双缓冲(Double Buffer)”,描述正确的是:
- A. 只能同时有一个Buffer被使用
- B. 前缓冲区负责显示,后缓冲区负责渲染,定时交换
- C. 只用来存储GPU的临时数据
- D. 只对SurfaceView生效
答案:B
解析:双缓冲本质是显示/渲染隔离,避免显示撕裂,A、C、D描述错误。
二、多选题(每题2分)
-
关于三缓冲(Triple Buffering)机制,以下说法正确的是:
- A. 能提升高帧率下的流畅性,减少卡顿感知
- B. 系统内会多维护一个Buffer,缓解帧处理高峰
- C. 一定能彻底避免丢帧
- D. 三缓冲下,部分jank帧不一定会引发可见卡顿
答案:A、B、D
解析:三缓冲提供更高的容错,缓冲帧丢失风险,但无法彻底避免丢帧(C错误)。 -
以下属于Surface的核心作用的是:
- A. 应用端的渲染入口(生产者角色)
- B. 提供Canvas接口给App绘图
- C. 直接负责合成所有App的图像
- D. 提供BufferQueue与SurfaceFlinger通信
答案:A、B、D
解析:Surface本身不做合成,合成由SF完成。
三、判断题(每题1分)
-
“Vsync信号的出现,能完全消除Android上的所有卡顿现象。” ( )
答案:错
解析:Vsync只是同步帧生成和刷新,无法消除业务/渲染本身慢导致的卡顿。 -
“普通View只能在主线程绘制,而SurfaceView可以在任意线程进行绘制。” ( )
答案:对
解析:SurfaceView可用子线程控制帧率,自主渲染,普通View需主线程。 -
“SurfaceFlinger是Android所有应用渲染数据的最终合成者。” ( )
答案:对
解析:这是Android图形架构的核心设计。
四、简答题(每题4分)
-
简述Vsync信号在Android图形系统中的作用,并说明其如何避免画面撕裂和卡顿。
答案解析:
Vsync信号用于同步系统帧的生成与屏幕的刷新。每当屏幕准备刷新新一帧内容时,Vsync信号触发,通知App/UI线程准备下一帧数据。这样做的好处是:- 避免在一帧未完成时切换到新帧,从而防止撕裂(同一时刻屏幕显示了两帧的内容)。
- 统一调度生产者(应用绘制)和消费者(屏幕显示),保证画面流畅,减少不必要的重绘,降低卡顿发生概率。
但Vsync无法完全杜绝因业务或渲染慢带来的丢帧问题。
-
请简述SurfaceView与普通View的本质区别,以及适用场景。
答案解析:
- 普通View只能在主线程上通过onDraw方法绘制,由系统统一调度刷新,适合UI控件、普通动画等场景。
- SurfaceView为App单独开辟画布(Surface),支持在子线程进行渲染绘制,帧率和刷新时机可控,适用于高帧率、视频播放、游戏等场景。SurfaceView与系统主UI线程解耦,不易因主线程卡顿而掉帧。
-
请结合BufferQueue的状态流转,简述生产者-消费者模型在Android图像合成流程中的体现。
答案解析:
- BufferQueue内部有多个BufferSlot,App端(生产者)通过dequeue获取Free Slot,进行绘制后通过queue提交到队列。
- SurfaceFlinger(消费者)通过acquire取走已完成的Buffer,合成输出到屏幕。合成完成后Buffer被释放回Free Slot,继续循环。
- 这种模式保证了App与SF的解耦,提高并发和帧处理容错性。