Android图形显示系统浅析

889 阅读9分钟

使用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


学后检测

一、单选题(每题1分)

  1. 下列关于Vsync信号的描述,正确的是?

    • A. Vsync信号只在GPU和显示屏之间同步
    • B. Vsync信号的目的是让App帧生成速度快于屏幕刷新速度
    • C. Vsync信号的本质是让系统帧速率与屏幕刷新率保持一致
    • D. Vsync信号只在系统出现卡顿时才会触发

    答案:C
    解析:Vsync的目的是让系统生成帧与屏幕刷新同步,避免撕裂和卡顿;A、B、D都错误或片面。

  2. 下列哪种情况最容易导致屏幕“撕裂”?

    • A. 系统帧速率远小于屏幕刷新率
    • B. 屏幕刷新率远大于系统帧速率
    • C. 屏幕刷新率和系统帧速率一样
    • D. 系统帧速率大于屏幕刷新率

    答案:D
    解析:系统帧生成太快,屏幕来不及刷新就被新帧覆盖,会出现“撕裂”现象。

  3. SurfaceFlinger的主要作用是什么?

    • A. 直接渲染App的UI到屏幕
    • B. 合成各个App窗口的图像并最终输出到屏幕
    • C. 负责内存分配和垃圾回收
    • D. 管理SurfaceView的生命周期

    答案:B
    解析:SF负责把所有窗口(Surface)合成,并最终显示到屏幕上。

  4. 关于Android中的“双缓冲(Double Buffer)”,描述正确的是:

    • A. 只能同时有一个Buffer被使用
    • B. 前缓冲区负责显示,后缓冲区负责渲染,定时交换
    • C. 只用来存储GPU的临时数据
    • D. 只对SurfaceView生效

    答案:B
    解析:双缓冲本质是显示/渲染隔离,避免显示撕裂,A、C、D描述错误。


二、多选题(每题2分)

  1. 关于三缓冲(Triple Buffering)机制,以下说法正确的是:

    • A. 能提升高帧率下的流畅性,减少卡顿感知
    • B. 系统内会多维护一个Buffer,缓解帧处理高峰
    • C. 一定能彻底避免丢帧
    • D. 三缓冲下,部分jank帧不一定会引发可见卡顿

    答案:A、B、D
    解析:三缓冲提供更高的容错,缓冲帧丢失风险,但无法彻底避免丢帧(C错误)。

  2. 以下属于Surface的核心作用的是:

    • A. 应用端的渲染入口(生产者角色)
    • B. 提供Canvas接口给App绘图
    • C. 直接负责合成所有App的图像
    • D. 提供BufferQueue与SurfaceFlinger通信

    答案:A、B、D
    解析:Surface本身不做合成,合成由SF完成。


三、判断题(每题1分)

  1. “Vsync信号的出现,能完全消除Android上的所有卡顿现象。” ( )

    答案:错
    解析:Vsync只是同步帧生成和刷新,无法消除业务/渲染本身慢导致的卡顿。

  2. “普通View只能在主线程绘制,而SurfaceView可以在任意线程进行绘制。” ( )

    答案:对
    解析:SurfaceView可用子线程控制帧率,自主渲染,普通View需主线程。

  3. “SurfaceFlinger是Android所有应用渲染数据的最终合成者。” ( )

    答案:对
    解析:这是Android图形架构的核心设计。


四、简答题(每题4分)

  1. 简述Vsync信号在Android图形系统中的作用,并说明其如何避免画面撕裂和卡顿。

    答案解析
    Vsync信号用于同步系统帧的生成与屏幕的刷新。每当屏幕准备刷新新一帧内容时,Vsync信号触发,通知App/UI线程准备下一帧数据。这样做的好处是:

    • 避免在一帧未完成时切换到新帧,从而防止撕裂(同一时刻屏幕显示了两帧的内容)。
    • 统一调度生产者(应用绘制)和消费者(屏幕显示),保证画面流畅,减少不必要的重绘,降低卡顿发生概率。
      但Vsync无法完全杜绝因业务或渲染慢带来的丢帧问题。
  2. 请简述SurfaceView与普通View的本质区别,以及适用场景。

    答案解析

    • 普通View只能在主线程上通过onDraw方法绘制,由系统统一调度刷新,适合UI控件、普通动画等场景。
    • SurfaceView为App单独开辟画布(Surface),支持在子线程进行渲染绘制,帧率和刷新时机可控,适用于高帧率、视频播放、游戏等场景。SurfaceView与系统主UI线程解耦,不易因主线程卡顿而掉帧。
  3. 请结合BufferQueue的状态流转,简述生产者-消费者模型在Android图像合成流程中的体现。

    答案解析

    • BufferQueue内部有多个BufferSlot,App端(生产者)通过dequeue获取Free Slot,进行绘制后通过queue提交到队列。
    • SurfaceFlinger(消费者)通过acquire取走已完成的Buffer,合成输出到屏幕。合成完成后Buffer被释放回Free Slot,继续循环。
    • 这种模式保证了App与SF的解耦,提高并发和帧处理容错性。