使用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?
下面给你一套Android VSync、BufferQueue、SurfaceFlinger、SurfaceView 核心题目,控制在 18 道左右,尽量覆盖你上面整理的知识点。每道题后面都直接带答案 + 解析。
Android VSync 与 SurfaceFlinger 核心题目
1. 为什么显示系统需要 VSync 信号?
答案:
因为屏幕刷新和 App 绘制是两个不同节奏的过程。如果没有统一调度信号,就可能出现:
1. 屏幕刷新太快,App 新帧没准备好 → 卡顿
2. App 绘制太快,屏幕还没刷完上一帧 → 撕裂
所以系统需要 VSync 信号,让 App、CPU、GPU、SurfaceFlinger 和 Display 按统一节奏协同工作。
解析:
屏幕显示内容时,会从 FrameBuffer 中自上而下、从左到右扫描像素。如果 App 在屏幕扫描过程中修改了同一块 Buffer,就可能导致屏幕上半部分还是旧帧,下半部分已经是新帧,这就是屏幕撕裂。
VSync 的核心作用就是:
用一个垂直同步信号,统一控制“什么时候开始生产新一帧、什么时候合成、什么时候显示”。
一句话总结:
VSync 是为了解决显示刷新和应用绘制不同步的问题。
2. 什么是屏幕撕裂?它是怎么产生的?
答案:
屏幕撕裂就是同一屏幕画面中同时出现了两帧内容,比如上半部分是旧帧,下半部分是新帧,导致画面割裂。
解析:
屏幕显示不是一次性把整张图瞬间显示出来,而是从 FrameBuffer 中逐行扫描:
从上到下
从左到右
逐行读取像素
如果屏幕正在读取 Buffer 的时候,App 又往同一块 Buffer 写入新内容,就会出现:
屏幕上半部分:读取的是旧内容
屏幕下半部分:读取的是新内容
这就是屏幕撕裂。
本质原因是:
显示端正在读 Buffer,应用端同时写 Buffer,产生读写冲突。
3. 前缓冲区和后缓冲区分别有什么作用?
答案:
前缓冲区 FrontBuffer:负责当前正在显示的内容
后缓冲区 BackBuffer:负责 App 正在绘制的下一帧内容
解析:
双缓冲的目的就是避免屏幕读和 App 写发生在同一个 Buffer 上。
正常流程是:
Display 从 FrontBuffer 读取当前帧
App / GPU 往 BackBuffer 写入下一帧
下一次 VSync 到来时,FrontBuffer 和 BackBuffer 交换角色
这样可以避免:
屏幕正在读,App 同时写
从而降低屏幕撕裂问题。
所以双缓冲的核心思想是:
显示读一块,应用写另一块,互不干扰。
4. 屏幕刷新率和系统帧率有什么区别?
答案:
- 屏幕刷新率 Hz:屏幕每秒刷新多少次。
- 系统帧率 FPS:App 每秒生成多少帧画面。
解析:
比如 60Hz 表示:
屏幕每秒刷新 60 次
每帧间隔约 16.67ms
而 60FPS 表示:
App 每秒生成 60 帧内容
两者不一定天然一致。
如果屏幕刷新快于 App 绘制:
下一帧没准备好,只能重复上一帧 → 卡顿
如果 App 绘制快于屏幕刷新:
新帧来得太快,屏幕还没显示完旧帧 → 可能撕裂
所以需要 VSync 把两者同步起来。
5. 为什么说 VSync 背后是生产者-消费者模型?
答案:
因为 App 负责生产图像 Buffer,SurfaceFlinger / Display 负责消费这些 Buffer 并显示到屏幕上。
解析:
可以这样理解:
生产者:App / CPU / GPU
消费者:SurfaceFlinger / Display
中间缓冲:BufferQueue
如果生产太慢:
消费者没有新 Buffer 可用 → 重复旧帧 → 卡顿
如果生产太快:
Buffer 堆积或者覆盖不当 → 可能撕裂 / 延迟 / 阻塞
所以 VSync 的意义是让生产和消费按统一节奏推进。
一句话总结:
VSync 是生产者和消费者之间的节拍器。
6. Android 中 VSync 主要协调哪些角色?
答案:
Android 中 VSync 主要协调:
Display:屏幕刷新
CPU:计算 UI、执行 measure/layout/draw
GPU:图形绘制、栅格化、渲染
SurfaceFlinger:合成多个 Surface
解析:
一帧画面从 App 到屏幕,大致经历:
CPU 计算 UI 数据
→ GPU 渲染图像
→ App 将 Buffer 提交给 BufferQueue
→ SurfaceFlinger 获取 Buffer
→ 合成多个窗口
→ Display 显示
如果没有 VSync,不同环节各跑各的,很容易出现节奏错乱。
所以 Android 通过 VSync 把这些环节串到同一个刷新节奏里。
7. 60Hz 屏幕下,为什么一帧大约是 16.67ms?
答案:
因为 60Hz 表示屏幕每秒刷新 60 次:
1000ms / 60 ≈ 16.67ms
解析:
在 60Hz 屏幕上,系统大约每 16.67ms 会有一次刷新机会。
如果 App 一帧的工作超过这个时间,比如:
measure/layout/draw 太慢
主线程被阻塞
GPU 渲染太慢
SurfaceFlinger 合成超时
就可能错过当前 VSync,导致这一帧无法按时显示。
这时屏幕只能继续显示上一帧,用户看到的就是卡顿或者掉帧。
8. 一帧绘制超时为什么会造成卡顿?
答案:
因为如果当前帧没有赶上 VSync,就不能在这次刷新中显示,只能等下一次 VSync。
解析:
假设 60Hz 屏幕每 16.67ms 刷新一次。
正常情况:
VSync 到来
→ App 开始处理一帧
→ CPU / GPU 完成绘制
→ SF 合成
→ 下一次 VSync 显示
如果某一帧处理时间过长:
当前 VSync 周期内没有准备好 Buffer
那么系统只能继续显示上一帧。
结果就是:
用户看到同一帧停留更久
视觉上表现为卡顿
所以 UI 优化的目标之一就是让每一帧尽量在 VSync 周期内完成。
9. 双缓冲机制有什么问题?
答案:
双缓冲虽然能减少撕裂,但在 CPU、GPU、SurfaceFlinger 并行工作时,容易出现 Buffer 不够用的问题,导致 pipeline 阻塞。
解析:
假设只有两个 Buffer:
Buffer A:正在被 SurfaceFlinger 合成
Buffer B:正在被 GPU 渲染
此时 CPU 想绘制下一帧,但是没有空闲 Buffer 可以使用。
结果就是:
CPU 只能等待
GPU / SF 也可能相互等待
整个渲染流水线阻塞
所以双缓冲的问题不是不能工作,而是在复杂并行场景下容易出现等待,无法充分利用 CPU、GPU、SF 的并行能力。
10. 三缓冲机制解决了什么问题?
答案:
三缓冲通过增加一个 Buffer,减少 CPU、GPU、SurfaceFlinger 之间互相等待的情况,让渲染流水线更容易并行推进。
解析:
三缓冲下可以出现这样的情况:
Buffer A:SurfaceFlinger 正在合成
Buffer B:GPU 正在渲染
Buffer C:CPU 可以继续准备下一帧
相比双缓冲,三缓冲的优势是:
1. 减少没有 Free Buffer 的情况
2. 减少 pipeline 阻塞
3. 提高 CPU / GPU / SF 并行度
4. 降低实际丢帧概率
一句话总结:
三缓冲不是让每一帧都更快,而是让渲染流水线更不容易卡住。
11. 三缓冲对 Jank 有什么影响?
答案:
三缓冲可以降低实际丢帧概率,让画面视觉上更平滑,但它不能从根本上消除 Jank。
解析:
如果某一帧稍微超时,在双缓冲场景下可能立刻导致后续环节等待。
而三缓冲因为多了一个 Buffer,可以让 CPU 继续生产下一帧,减少 pipeline 卡死的概率。
但是如果 App 持续耗时,比如:
主线程长期阻塞
GPU 持续渲染过重
SF 合成一直超时
三缓冲也救不了。
所以三缓冲的作用是:
缓解短暂波动造成的卡顿,而不是解决所有性能问题。
12. SurfaceFlinger 在 Android 渲染体系中负责什么?
答案:
SurfaceFlinger 是系统级合成服务,负责把所有 App 和系统窗口对应的 Surface 合成为最终屏幕画面。
解析:
一个屏幕上可能同时有很多 Surface:
Activity 主窗口 Surface
状态栏 Surface
导航栏 Surface
输入法 Surface
Dialog Surface
PopupWindow Surface
每个 App 或系统窗口先把自己的内容画到自己的 Surface 中。
然后 SurfaceFlinger 统一负责:
收集各个 Surface 的 Buffer
根据窗口层级、位置、透明度等信息进行合成
最终输出到屏幕
一句话总结:
App 负责画自己的窗口,SurfaceFlinger 负责把所有窗口合成成最终屏幕。
13. Window、Surface、BufferQueue 之间是什么关系?
答案:
通常可以这样理解:
一个 Window 对应一个 Surface
一个 Surface 背后对应一个 BufferQueue
解析:
更准确地说,App 端通过 Surface 向 BufferQueue 申请 Buffer,绘制完成后再把 Buffer 放回队列,等待 SurfaceFlinger 消费。
整体关系是:
Window
→ Surface
→ BufferQueue
→ Buffer
App 和 SurfaceFlinger 之间不是直接共享随意访问的内存,而是通过 BufferQueue 建立生产者-消费者关系。
这样设计的好处是:
1. 解耦 App 和系统合成服务
2. 保证跨进程图形数据传递的安全性
3. 避免 App 直接操作系统合成资源
4. 方便同步和调度 Buffer 状态
14. BufferQueue 中 Producer 和 Consumer 分别是谁?
答案:
Producer:App 端
Consumer:SurfaceFlinger
解析:
App 端作为生产者,会从 BufferQueue 中取出可用 Buffer:
dequeueBuffer
然后 App / GPU 往 Buffer 中绘制内容。
绘制完成后,App 把 Buffer 放回队列:
queueBuffer
SurfaceFlinger 作为消费者,会从 BufferQueue 中取出已经绘制完成的 Buffer:
acquireBuffer
合成完成后再释放:
releaseBuffer
整体流程是:
App 取 Buffer
→ App 绘制
→ App 入队
→ SF 获取
→ SF 合成
→ Buffer 释放回队列
15. Buffer 的状态流转过程是什么?
答案:
Buffer 的典型状态流转是:
Free → Dequeued → Queued → Acquired → Free
解析:
每个状态含义如下:
| 状态 | 含义 |
|---|---|
Free | 空闲状态,可以被 App 获取 |
Dequeued | App 已经取到 Buffer,正在绘制 |
Queued | App 绘制完成,提交到队列,等待 SF 合成 |
Acquired | SF 已经获取 Buffer,正在合成 |
Free | SF 合成完成后释放,重新变为空闲 |
可以结合 App 和 SF 的流程理解:
App dequeue:Free → Dequeued
App queue:Dequeued → Queued
SF acquire:Queued → Acquired
SF release:Acquired → Free
这个状态机保证了 Buffer 不会被 App 和 SF 同时乱用。
16. 为什么说正常情况下 Queue 中最好只有一个 Buffer?
答案:
因为 Queue 中 Buffer 太多,说明 App 生产的帧已经积压,SurfaceFlinger 来不及消费,可能意味着卡顿或延迟。
解析:
正常情况下:
App 生产一帧
SF 消费一帧
队列中不应该长期堆积多个待消费 Buffer。
如果 Queued 状态的 Buffer 很多,说明:
App 已经提交了多帧
但 SF 没有及时合成消费
这会带来两个问题:
1. 画面显示延迟变大
2. 后续 Buffer 可能不够用,导致生产端阻塞
所以 BufferQueue 虽然可以容纳多个 Slot,但不代表队列越满越好。
一句话总结:
BufferQueue 是为了平滑生产消费,不是为了无限缓存旧帧。
17. SurfaceView 和普通 View 有什么区别?
答案:
SurfaceView 拥有独立的 Surface,可以在子线程绘制,也可以手动控制帧率;普通 View 一般依赖宿主 Window 的 View 树绘制流程,通常在主线程绘制。
解析:
普通 View 的绘制流程是:
ViewRootImpl
→ performTraversals()
→ measure
→ layout
→ draw
它和整个 View 树一起绘制,通常由主线程调度。
SurfaceView 则更特殊:
它在 Window 中占一个位置
但绘制内容可以走独立 Surface
所以常说 SurfaceView 像是:
在 Window 上挖了一个洞,专门给自己的 Surface 显示内容。
它适合:
游戏
视频播放
相机预览
高频绘制场景
因为这些场景不适合完全依赖普通 View 的主线程绘制节奏。
18. SurfaceView 中 lockCanvas() 和 unlockCanvasAndPost() 分别做什么?
答案:
lockCanvas():从 Surface 对应的 BufferQueue 中取出 Buffer,并返回 Canvas 用于绘制
unlockCanvasAndPost():绘制完成后提交 Buffer,等待 SurfaceFlinger 合成
解析:
典型代码:
private void drawUI() {
Canvas canvas = holder.lockCanvas(); // dequeue
try {
drawCanvas(canvas);
} finally {
holder.unlockCanvasAndPost(canvas); // queue
}
}
可以理解为:
lockCanvas()
→ dequeueBuffer
→ 拿到可绘制 Canvas
drawCanvas()
→ 在 Buffer 上绘制内容
unlockCanvasAndPost()
→ queueBuffer
→ 提交给 SurfaceFlinger
这就是 SurfaceView 可以在子线程循环绘制的核心原因。
普通 View 的绘制由 ViewRootImpl 统一调度,而 SurfaceView 可以自己控制绘制循环和提交节奏。
总结记忆版
19. Android 一帧从 App 到屏幕的大致流程是什么?
答案:
可以概括为:
VSync 到来
→ Choreographer 调度 App 绘制
→ ViewRootImpl 执行 performTraversals()
→ measure / layout / draw
→ App 通过 Surface 获取 Buffer
→ CPU / GPU 绘制内容
→ queueBuffer 提交
→ SurfaceFlinger acquireBuffer
→ 合成多个 Surface
→ Display 显示到屏幕
解析:
这条链路里有几个核心对象:
| 对象 | 作用 |
|---|---|
VSync | 提供统一刷新节奏 |
Choreographer | 在 App 侧接收 VSync 并调度绘制任务 |
ViewRootImpl | 执行 View 树遍历 |
Surface | App 绘制内容的载体 |
BufferQueue | 连接 App 和 SF 的缓冲队列 |
SurfaceFlinger | 消费 Buffer 并合成最终屏幕 |
Display | 显示最终画面 |
最终可以记成一句话:
App 负责生产每个窗口的 Buffer,
SurfaceFlinger 负责消费并合成所有 Buffer,
VSync 负责让整个生产消费过程按屏幕刷新节奏推进。