Android图形显示系统浅析

1,054 阅读16分钟

使用Vsync信号的由来

显示屏上的内容,其实是从硬件的帧缓冲区(FrameBuffer)中逐行读取的。大致过程如下:

  • 从Buffer的起始地址开始,自上而下、从左到右扫描,将内容绘制到屏幕。
  • 这个“图像内容”就来自于App端写入的数据。

image.png

缓冲区读写冲突与屏幕撕裂

如果对同一个缓冲区同时进行读写,就会出现“屏幕撕裂”(Tearing):

  • 即同一时刻,屏幕上方是上一帧内容,下方是新帧内容,画面割裂、不连贯。

解决思路:前后缓冲区

为了解决这个问题,引入了**前缓冲区(FrontBuffer)和后缓冲区(BackBuffer)**的概念:

image.png

前缓冲区提供当前帧的显示,后缓冲区负责下一帧的渲染。某个时刻二者交换角色,实现无缝切换和显示。

image.png


屏幕刷新率与系统帧率

  • 屏幕刷新率(Hz) :每秒刷新屏幕的次数(一般60Hz=16.67ms/帧)
  • 系统帧速率(FPS) :每秒应用生成帧的次数

两种极端情况

  1. 屏幕刷新快于系统绘制:下一帧没准备好,只能重复上一帧内容——画面“卡顿”。
  2. 系统绘制快于屏幕刷新:还没等前帧显示完,后帧已就绪,前后混合显示——画面“撕裂”。

根本原因: 两者速率不一致,缺乏统一“调度”信号。


供需失衡的本质

本质上就是生产者-消费者模型失衡。必须有机制保证系统合成帧和屏幕刷新同步。

于是——Vsync信号诞生:每到一次Vsync信号(Vertical Synchronization,垂直同步),应用才开始合成和显示新一帧。

image.png
image.png
image.png


Android上的Vsync

Android系统引入Vsync同步,来协调Display(屏幕)CPU(合成/生成图片)GPU(绘制/光栅化) 等各方工作。

image.png

  • Display:显示屏
  • CPU:计算像素、生成位图
  • GPU:图形加速、栅格化
  • DoubleBuffer:缓冲区回收利用,性能优化

image.png

绘制超时与卡顿

如果合成一帧超出Vsync间隔,下一帧会延迟到下一个Vsync才显示,造成“卡顿”。

image.png


三缓冲(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 可用,不卡死。

image.png

三缓冲对Jank的影响

即使部分帧合成超时(理论上会jank),三缓冲可以减少真正的“丢帧”几率,视觉停留更平滑。


SurfaceFlinger(SF)合成机制

举个例子:

image.png

  • **每个界面窗口(Window)**对应SurfaceFlinger里的一个Surface对象。
  • App端把内容渲染到自己的Surface。
  • SF负责将所有Surface进行合成(Compose) ,再输出到屏幕。

Surface & BufferQueue

  • 一个Window <=> 一个Surface <=> 一个BufferQueue
    Surface不是直接持有BufferQueue,必须通过“生产者”获取。

image.png
image.png

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

image.png

BufferQueue的Slot数目默认64。

image.png

未使用(Unused Slot)与已用(Use slot/unactive/active)状态区分。


  • App端Surface通过Producer从BufferQueue取“FreeSlot”缓冲区渲染数据;
  • 渲染后通过Producer返回队列,等待SF消费。

image.png

整体合成架构:

image.png

  • View的draw方法进行dequeue,写入数据,queue后SF合成。
  • 之所以设计成“生产者-消费者”,是为了解耦安全,App无法直接访问系统合成资源。

image.png

正常情况下,Queue只应有1个Buffer。如果出现多个,说明“卡顿”。

image.png


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?

image.png


下面给你一套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 获取
DequeuedApp 已经取到 Buffer,正在绘制
QueuedApp 绘制完成,提交到队列,等待 SF 合成
AcquiredSF 已经获取 Buffer,正在合成
FreeSF 合成完成后释放,重新变为空闲

可以结合 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 树遍历
SurfaceApp 绘制内容的载体
BufferQueue连接 App 和 SF 的缓冲队列
SurfaceFlinger消费 Buffer 并合成最终屏幕
Display显示最终画面

最终可以记成一句话:

App 负责生产每个窗口的 Buffer,
SurfaceFlinger 负责消费并合成所有 Buffer,
VSync 负责让整个生产消费过程按屏幕刷新节奏推进。