Android 图形系统:Canvas、Skia、Surface 与 SurfaceFlinger

上一篇介绍了 Activity、Window、DecorView 和 ViewRootImpl 的关系。

当 ViewRootImpl 发起 draw() 后,问题就从“界面如何组织”进入了“图像如何生成”:

  • View 为什么需要 Canvas?
  • Canvas 是不是直接操作屏幕?
  • Skia、OpenGL ES 和 Vulkan 是什么关系?
  • Surface 到底保存了什么?
  • WindowManagerService 和 SurfaceFlinger 为什么缺一不可?
  • SurfaceView 为什么能让视频解码器直接输出画面?

本文从一次 View 绘制开始,把 Android 图形系统的主链路串起来。


一、先看完整渲染链路

flowchart TD
    A[ViewRootImpl 发起 draw] --> B[View 树]
    B --> C[Canvas 绘图接口]
    C --> D{绘制模式}

    D -->|软件绘制| E[Skia Raster]
    E -->|CPU 光栅化| J[Surface Buffer]

    D -->|硬件加速| F[RenderNode / DisplayList]
    F --> G[HWUI / RenderThread]
    G --> H[Skia GPU]
    H --> I[OpenGL ES 或 Vulkan]
    I --> J

    J --> K[BufferQueue]
    K --> L[SurfaceFlinger]
    M[WindowManagerService] -->|窗口位置、层级和裁剪信息| L
    L --> N[GPU 或 Hardware Composer 合成]
    N --> O[屏幕]

可以先记住:

View 通过 Canvas 描述要画什么,Skia 和图形后端生成像素,Surface 提交图形 Buffer,SurfaceFlinger 合成所有图层并输出到屏幕。


二、Canvas:描述“画什么”的绘图接口

自定义 View 通常会重写 onDraw()

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    canvas.drawCircle(
        width / 2f,
        height / 2f,
        100f,
        paint
    )
}

Canvas 提供了常见绘图能力:

  • 绘制文字;
  • 绘制矩形、圆形和路径;
  • 绘制 Bitmap;
  • 裁剪绘制区域;
  • 平移、旋转和缩放坐标系;
  • 设置图层和混合效果。

Canvas 的职责是提供统一的二维绘图 API。

它描述的是:

需要绘制什么图形,以及用什么坐标和参数绘制。

Canvas 本身不是屏幕,也不是固定的 GPU API。

同样的 Canvas API,背后可能连接:

  • CPU 软件光栅化;
  • GPU 硬件加速;
  • Bitmap;
  • Surface;
  • RenderNode 记录过程。

因此不能简单理解为:

flowchart LR
    A[Canvas] --> B[直接修改屏幕像素]

正确关系是:

flowchart LR
    A[View] --> B[Canvas API]
    B --> C[渲染实现]
    C --> D[图形 Buffer]

三、Skia:Android 二维图形引擎

Skia 是一个开源二维图形库,也是 Android 图形系统的重要基础组件。

它主要处理:

  • 路径和几何图形;
  • 字体和文字排版;
  • 图片绘制;
  • 颜色和渐变;
  • Shader;
  • 混合模式;
  • 抗锯齿;
  • 图像滤镜;
  • 光栅化。

Canvas 解决的是“用什么 API 描述绘制”,Skia 解决的是“怎样把这些二维图形转换成可显示结果”。

flowchart LR
    A[Canvas 绘图命令] --> B[Skia]
    B --> C[计算路径、文字、颜色和混合]
    C --> D[选择 CPU 或 GPU 后端]

Skia 并不等于 OpenGL ES。

Skia 可以使用不同后端:

flowchart TD
    A[Skia 绘图任务] --> B{选择渲染后端}
    B --> C[Raster CPU]
    B --> D[OpenGL GPU]
    B --> E[Vulkan GPU]

因此,“View 渲染一定经过 Canvas、Skia、OpenGL ES”只说对了一部分。

更准确的结论是:

View 通常通过 Canvas 描述绘制,由 Skia 参与二维图形处理;最终可能使用 CPU、OpenGL ES 或 Vulkan 完成光栅化。


四、软件绘制:CPU 直接生成像素

关闭硬件加速,或者某一部分使用软件图层时,会走软件绘制路径。

flowchart TD
    A[ViewRootImpl] --> B[View.draw]
    B --> C[View.onDraw]
    C --> D[Software Canvas]
    D --> E[Skia Raster]
    E -->|CPU 计算像素| F[内存 Buffer]
    F --> G[通过 Surface 提交]

这条链路的主要特点是:

  • View 的 Canvas 操作由 CPU 执行;
  • Skia 的 Raster 后端负责光栅化;
  • 结果直接写入 CPU 可访问的内存 Buffer;
  • 不要求经过 OpenGL ES;
  • 复杂页面可能给 UI 线程和 CPU 带来较大压力。

软件绘制可以简化为:

flowchart LR
    A[Canvas] --> B[Skia Raster]
    B --> C[CPU]
    C --> D[Surface Buffer]

五、硬件加速:先记录,再由渲染线程执行

现代 Android 应用一般默认开启硬件加速。

硬件加速时,View.onDraw(canvas) 仍然会执行,但 Canvas 调用通常不会立即逐像素绘制。

它们首先被记录成可复用的绘制指令。

flowchart TD
    A[View.onDraw] --> B[Hardware Canvas]
    B --> C[记录 drawRect drawText 等操作]
    C --> D[RenderNode / DisplayList]
    D --> E[HWUI]
    E --> F[RenderThread]
    F --> G[Skia GPU]
    G --> H[OpenGL ES 或 Vulkan]
    H --> I[GPU]
    I --> J[Surface Buffer]

这套设计分成两个主要阶段。

阶段一:UI 线程记录绘制命令

flowchart LR
    A[View.onDraw] --> B[Canvas.drawXXX]
    B --> C[RecordingCanvas]
    C --> D[RenderNode]

例如:

canvas.drawRect(...)
canvas.drawText(...)
canvas.drawBitmap(...)

这些调用更像是在生成一份绘图指令,而不是每次调用都立即写出最终像素。

阶段二:RenderThread 执行渲染

flowchart LR
    A[RenderNode / DisplayList] --> B[HWUI]
    B --> C[RenderThread]
    C --> D[Skia]
    D --> E[OpenGL ES / Vulkan]
    E --> F[GPU]
    F --> G[Surface Buffer]

这样做的好处包括:

  • 未发生变化的 RenderNode 可以复用;
  • 一部分渲染工作可以从 UI 线程移出;
  • 动画属性可以更高效地更新;
  • GPU 更适合并行执行大量像素和几何计算。

不过,硬件加速不代表 CPU 完全不参与。

View 遍历、绘图命令记录、文字处理、路径准备、资源上传和驱动调用等环节仍可能使用 CPU。


六、OpenGL ES 和 Vulkan 在什么位置?

OpenGL ES 和 Vulkan 都属于较底层的 GPU 图形 API。

它们主要负责:

  • 管理纹理和缓冲区;
  • 创建和执行 Shader;
  • 向 GPU 提交绘制命令;
  • 管理同步;
  • 把渲染结果写入目标 Buffer。

Skia 与 OpenGL ES、Vulkan 的关系可以理解为:

flowchart TD
    A[Skia] --> B[理解二维图形语义]
    B --> C[生成底层渲染任务]
    C --> D{GPU API}
    D --> E[OpenGL ES]
    D --> F[Vulkan]
    E --> G[GPU 驱动]
    F --> G

Skia 更关注“文字、路径、图片和颜色应该如何绘制”,OpenGL ES 或 Vulkan 更关注“怎样把任务提交给 GPU”。

因此它们不是并列替代关系,而是不同抽象层次上的组件。


七、Surface:图形 Buffer 的生产端接口

Surface 经常被误解成“屏幕上的一块区域”。

更准确地说:

Surface 是应用向 BufferQueue 申请、写入并提交图形 Buffer 的生产端接口。

应用不会直接修改物理屏幕。

绘制结果需要先进入 Buffer:

flowchart TD
    A[CPU 或 GPU 生成一帧] --> B[Surface]
    B --> C[获取可写 Buffer]
    C --> D[完成绘制]
    D --> E[提交 Buffer]
    E --> F[BufferQueue]

Surface 自己并不是一张永久固定的 Bitmap。

它背后关联的是一套 BufferQueue 和多个可轮换使用的图形 Buffer。


八、BufferQueue:连接生产者和消费者

Android 图形数据大量依赖 BufferQueue 传递。

BufferQueue 一端是生产者,另一端是消费者。

flowchart LR
    A[生产者] -->|dequeueBuffer| B[BufferQueue]
    A -->|绘制完成后 queueBuffer| B
    B -->|acquireBuffer| C[消费者]
    C -->|使用完成后 releaseBuffer| B

典型生产者包括:

  • Android UI 渲染系统;
  • OpenGL ES;
  • Vulkan;
  • Camera;
  • MediaCodec 视频解码器。

典型消费者包括:

  • SurfaceFlinger;
  • SurfaceTexture;
  • ImageReader;
  • 视频编码器。

普通 Activity 主窗口中,应用渲染系统通常是生产者,SurfaceFlinger 是消费者。

flowchart LR
    A[应用 UI 渲染] --> B[Surface / BufferQueue]
    B --> C[SurfaceFlinger]

BufferQueue 的价值是:

  • 生产和消费可以异步进行;
  • 避免生产者直接修改正在显示的 Buffer;
  • 支持多缓冲;
  • 通过 Fence 处理 GPU 和显示系统之间的同步;
  • 减少不必要的数据复制。

九、Surface 与 ViewRootImpl 的关系

ViewRootImpl 负责驱动 View 树,Surface 负责承接渲染结果。

flowchart LR
    A[ViewRootImpl] -->|发起 measure layout draw| B[View 树]
    B --> C[Canvas / RenderNode]
    C --> D[Skia / HWUI]
    D -->|写入| E[Surface Buffer]

ViewRootImpl 本身不是二维图形引擎,也不是 Buffer。

它的职责是:

  • 管理根 View;
  • 调度 View 遍历;
  • 管理窗口尺寸和 Insets;
  • 与窗口系统通信;
  • 协调窗口使用的 Surface 相关资源;
  • 在 Surface 有效时发起绘制。

可以概括为:

ViewRootImpl 决定什么时候画,View 和 Canvas 描述画什么,Skia 与图形后端负责怎么画,Surface 决定画面提交到哪里。


十、WindowManagerService:管理窗口关系

WindowManagerService 不负责逐像素绘制 View。

它主要管理:

  • 窗口位置;
  • 窗口尺寸;
  • 窗口层级;
  • 窗口可见性;
  • 窗口焦点;
  • 屏幕旋转;
  • 分屏和画中画;
  • 窗口权限;
  • 窗口裁剪和 Insets;
  • 窗口转场。
flowchart TD
    A[WindowManagerService] --> B[窗口位置]
    A --> C[窗口大小]
    A --> D[窗口层级]
    A --> E[窗口焦点]
    A --> F[窗口可见性]

WMS 解决的是:

每个窗口应该怎样存在于系统中。


十一、SurfaceFlinger:合成所有可见图层

每个应用只会生成自己的窗口内容,但屏幕上还可能有:

  • 状态栏;
  • 导航栏;
  • 输入法;
  • 其他应用窗口;
  • Dialog;
  • SurfaceView;
  • 画中画窗口。

这些内容最终由 SurfaceFlinger 统一合成。

flowchart TD
    A[应用主窗口 Buffer] --> E[SurfaceFlinger]
    B[状态栏 Buffer] --> E
    C[输入法 Buffer] --> E
    D[SurfaceView Buffer] --> E

    F[WindowManagerService] -->|位置、层级、裁剪和变换信息| E

    E --> G[选择合成策略]
    G --> H[GPU RenderEngine 合成]
    G --> I[Hardware Composer 合成]
    H --> J[显示设备]
    I --> J

SurfaceFlinger 负责:

  • 获取各个可见图层的新 Buffer;
  • 根据窗口元数据设置图层位置和层级;
  • 处理裁剪、旋转、缩放和透明度;
  • 选择 GPU 或 Hardware Composer 进行合成;
  • 把最终画面发送到显示设备。

需要强调:

SurfaceFlinger 不理解 Button、TextView 或 RecyclerView,它只处理图层、Buffer 和显示属性。


十二、WMS 与 SurfaceFlinger 为什么都需要?

WMS 与 SurfaceFlinger 分别解决不同问题。

组件 负责的问题
WindowManagerService 窗口应该放在哪里、显示多大、位于哪一层、是否可见、谁有焦点
SurfaceFlinger 获取哪些图层 Buffer,并怎样把它们合成为最终画面

二者的协作关系是:

flowchart LR
    A[WindowManagerService] -->|窗口元数据| B[SurfaceFlinger]
    C[各个 Surface] -->|图形 Buffer| B
    B --> D[最终合成画面]
    D --> E[屏幕]

可以用一句话区分:

WMS 管窗口秩序,SurfaceFlinger 管图层合成。


十三、SurfaceView 为什么拥有独立 Surface?

普通 View 通常共享 Activity 主窗口的 Surface。

flowchart TD
    A[Activity 主 Surface]
    B[TextView] --> A
    C[ImageView] --> A
    D[RecyclerView] --> A
    E[普通自定义 View] --> A

而 SurfaceView 会创建一条独立的 Surface 和 BufferQueue 链路。

flowchart TD
    A[Activity Window] --> B[主窗口 Surface]
    A --> C[SurfaceView]
    C --> D[独立 Surface]
    D --> E[独立 SurfaceFlinger Layer]

    B --> F[SurfaceFlinger]
    E --> F
    F --> G[屏幕]

这让 SurfaceView 适合:

  • 视频播放;
  • Camera 预览;
  • 游戏画面;
  • 高频图像输出;
  • MediaCodec 直接输出。

例如视频解码器可以直接把画面写入 Surface:

flowchart LR
    A[MediaCodec] --> B[SurfaceView Surface]
    B --> C[BufferQueue]
    C --> D[SurfaceFlinger]
    D --> E[屏幕]

不需要先将每一帧转换成 Bitmap,再交给普通 View 绘制。


十四、TextureView 与 SurfaceView 的差别

TextureView 内部使用 SurfaceTexture 接收 Buffer,但它会把最新 Buffer 作为纹理参与普通 View 树绘制。

flowchart TD
    A[视频或相机生产者] --> B[SurfaceTexture BufferQueue]
    B --> C[TextureView 获取最新纹理]
    C --> D[参与普通 View 树渲染]
    D --> E[Activity 主 Surface]

SurfaceView 通常拥有独立图层:

flowchart TD
    A[视频或相机生产者] --> B[SurfaceView 独立 Surface]
    B --> C[独立 SurfaceFlinger Layer]

简单比较:

类型 图像如何进入屏幕 主要特点
普通 View 绘制到 Activity 主 Surface 适合普通 UI
TextureView 作为纹理参与主 View 树绘制 容易执行旋转、缩放、透明度等 View 变换
SurfaceView 独立 Surface 和独立图层 适合视频、相机和高频画面

SurfaceView 的独立图层通常效率更高,但它与普通 View 的同步、动画和叠加关系也更复杂。


十五、一帧是怎样显示出来的?

把 View 绘制与图层合成放到一帧中,可以得到下面的主链路:

sequenceDiagram
    participant VS as VSync
    participant C as Choreographer
    participant VRI as ViewRootImpl
    participant View as View 树
    participant RT as RenderThread
    participant Skia
    participant Surface
    participant SF as SurfaceFlinger
    participant HWC as Hardware Composer
    participant Display as 屏幕

    VS->>C: 帧同步信号
    C->>VRI: 执行当前帧回调
    VRI->>View: measure / layout / draw
    View->>RT: 提交 RenderNode 绘制内容
    RT->>Skia: 执行二维图形渲染
    Skia->>Surface: 写入并提交 Buffer
    Surface->>SF: 新 Buffer 可用
    SF->>HWC: 提交图层合成任务
    HWC->>Display: 输出最终画面

实际系统还会涉及:

  • FrameTimeline;
  • Sync Fence;
  • BLASTBufferQueue;
  • SurfaceControl;
  • RenderEngine;
  • EGL;
  • GPU 驱动;
  • HWC HAL。

但这些属于更深入的图形系统实现,不影响本文的主链路。


十六、常见误区

误区一:Canvas 就是 Skia

不准确。

Canvas 是绘图接口,Skia 是处理和执行二维图形任务的底层引擎之一。

误区二:Canvas 会直接画到屏幕

不准确。

Canvas 的结果还要经过光栅化、Surface Buffer 和 SurfaceFlinger 合成。

误区三:硬件加速后不再使用 Canvas

不准确。

View 仍通过 Canvas API 描述绘制,只是硬件 Canvas 通常会先记录绘制命令。

误区四:View 绘制一定经过 OpenGL ES

不准确。

软件绘制可以使用 CPU Raster,硬件绘制也可能使用 Vulkan。

误区五:Surface 是一块固定内存

不准确。

Surface 是 BufferQueue 生产端接口,背后通常有多块轮换使用的图形 Buffer。

误区六:SurfaceFlinger 负责绘制 View

不准确。

SurfaceFlinger 负责合成图层,不负责执行 TextView 或自定义 View 的 onDraw()

误区七:WMS 与 SurfaceFlinger 是同一个服务

不准确。

WMS 管窗口关系,SurfaceFlinger 管图形 Buffer 和图层合成。


十七、各组件职责总结

组件 核心职责
View 定义页面内容和绘制逻辑
Canvas 提供二维绘图命令
RenderNode / DisplayList 记录并复用硬件加速绘制操作
HWUI Android View 硬件加速渲染框架
RenderThread 执行应用 UI 渲染任务
Skia 处理文字、路径、图片和二维光栅化
OpenGL ES / Vulkan 将底层图形任务提交给 GPU
Surface BufferQueue 的生产端接口
BufferQueue 在图形生产者与消费者之间传递 Buffer
WindowManagerService 管理窗口位置、层级、焦点和权限
SurfaceFlinger 合成全部可见图层
Hardware Composer 使用显示硬件协助图层合成

十八、最终架构图

flowchart TD
    subgraph App[应用进程]
        A[ViewRootImpl]
        B[View 树]
        C[Canvas]
        D[RenderNode / DisplayList]
        E[HWUI / RenderThread]
        F[Skia]
        G[CPU / OpenGL ES / Vulkan]
        H[Surface BufferQueue]

        A -->|驱动| B
        B --> C
        C --> D
        D --> E
        E --> F
        F --> G
        G --> H
    end

    subgraph SystemServer[system_server]
        I[WindowManagerService]
    end

    subgraph Graphics[图形与显示系统]
        J[SurfaceFlinger]
        K[RenderEngine / Hardware Composer]
        L[显示设备]
    end

    H -->|提交 Buffer| J
    I -->|窗口元数据| J
    J --> K
    K --> L

总结

Android View 渲染到屏幕,可以拆成四步。

第一步:描述页面

flowchart LR
    A[View] --> B[Canvas]

View 通过 Canvas 描述文字、图片、路径和图形。

第二步:生成像素

flowchart TD
    A[Canvas] --> B{绘制模式}
    B -->|软件| C[Skia Raster + CPU]
    B -->|硬件| D[RenderNode + HWUI + Skia]
    D --> E[OpenGL ES / Vulkan + GPU]

第三步:提交画面

flowchart LR
    A[渲染结果] --> B[Surface]
    B --> C[BufferQueue]

Surface 将生成的图形 Buffer 提交给消费者。

第四步:合成到屏幕

flowchart LR
    A[多个 Surface Buffer] --> B[SurfaceFlinger]
    C[WindowManagerService] -->|窗口元数据| B
    B --> D[GPU / Hardware Composer]
    D --> E[屏幕]

完整过程可以浓缩成一句话:

View 通过 Canvas 描述绘制,Skia 使用 CPU 或 GPU 生成图像,Surface 通过 BufferQueue 提交图像,SurfaceFlinger 根据 WMS 提供的窗口信息完成合成并输出到屏幕。

若需要先理解 Activity、Window、DecorView 和 ViewRootImpl 的关系,请阅读:

Android 界面体系:Activity、Window、View 与 ViewRootImpl


参考资料

  1. Android Developers:硬件加速
  2. Android Developers:RenderNode API
  3. AOSP:Android 图形架构
  4. AOSP:SurfaceFlinger 与 WindowManager
  5. AOSP:BufferQueue 与 Gralloc
  6. AOSP:Surface 与 SurfaceHolder
  7. AOSP:SurfaceView 与 GLSurfaceView
  8. AOSP:TextureView
  9. AOSP:EGLSurface 与 OpenGL ES
  10. AOSP:Hardware Composer HAL
  11. Skia:SkCanvas Creation
  12. Skia API:SkCanvas

说明:Android 图形栈持续演进,不同 Android 版本和设备厂商可能采用不同的 GPU 后端、Buffer 管理与合成策略。本文重点描述稳定的架构职责和通用数据流。