Android的渲染机制

11 阅读10分钟

一、先记住这张总图

业务代码修改 View
        │
        ▼
invalidate / requestLayout
        │
        ▼
Choreographer 等待 App VSync
        │
        ▼
ViewRootImpl.performTraversals()
        │
        ├── measure:确定尺寸
        ├── layout :确定位置
        └── draw   :记录绘制指令
                    │
                    ▼
               RenderThread
                    │
                    ▼
           GPU/Skia 渲染到 Buffer
                    │
                    ▼
 Surface.queueBuffer → BufferQueue
                    │
                    ▼
            SurfaceFlinger
                    │
          收集系统所有可见 Layer
                    │
                    ▼
        HWC 决定每层的合成方式
             │              │
             │              │
      CLIENT 合成       DEVICE 合成
        GPU 合成          DPU/Overlay
             └──────┬───────┘
                    ▼
               Present/送显
                    │
                    ▼
                 屏幕

这里必须牢牢记住两种动作:

  • 渲染:把某个图层的绘制命令变成像素。
  • 合成:决定多个图层最终如何叠加显示。

例如:

  • App 把按钮、文字、图片画进自己的窗口 Buffer,是渲染。
  • SurfaceFlinger 把 App、状态栏、导航栏、壁纸叠起来,是合成。

要以“渲染—合成—送显”作为主线,Android 官方则把系统核心概括为 BufferQueue、SurfaceFlinger 和 Hardware Composer。AOSP Graphics Architecture


二、一帧究竟是怎样产生的

1. View 发生变化

典型入口是:

view.invalidate();
view.requestLayout();

两者不能混为一谈:

方法含义通常触发
invalidate()内容变了,需要重画draw
requestLayout()尺寸或位置可能变了measure + layout + draw

它们不会立刻完成屏幕绘制,而是通过 ViewRootImpl.scheduleTraversals(),将一次 traversal 安排到下一次合适的帧时机。

这样做是为了合并同一帧中的多次修改。例如连续设置三次文字,系统没必要立即向屏幕提交三帧,中间状态通常可以被合并掉。

2. Choreographer 等待 VSync

Choreographer 可以理解成 UI 线程的帧调度器。

它收到帧时序事件后,按阶段执行:

输入事件
→ 动画
→ View 树遍历
→ 提交渲染

文章重点讲了 traversal callback 和同步屏障:同步屏障使 VSync 驱动的异步消息可以优先于普通同步消息被处理。

但要避免一个误解:

VSync 不是“系统命令 App 必须画一帧”,而是给需要更新画面的参与者提供一个与屏幕刷新对齐的时间基准。

现代 Android 的 Choreographer 还包含 FrameTimeline、deadline 和 expected presentation time 等信息,不再只是一个简单的固定周期回调。Choreographer API

3. ViewRootImpl 遍历 View 树

VSync 回调最终推动:

ViewRootImpl.doTraversal()
└── performTraversals()
    ├── performMeasure()
    ├── performLayout()
    └── performDraw()

三阶段分别回答三个问题:

  • Measure:每个 View 多大?
  • Layout:每个 View 放在哪里?
  • Draw:它应该画什么?

注意,Measure 并不保证只遍历一次。某些布局可能要求父子之间反复测量,所以复杂嵌套、权重和频繁改变布局参数都会增加成本。


三、硬件加速下,draw 并不等于 GPU 正在画

这是原文最值得进一步校正的地方。

文章说“开启硬件加速后,draw 发生在 GPU”,作为入门近似可以接受,但不够精确。真实过程分成两个阶段:

UI Thread
View.draw(Canvas)
→ 将绘制操作记录为 RenderNode / DisplayList

RenderThread
→ 处理这些绘制命令
→ 使用 Skia/GPU 将内容栅格化到 Buffer

也就是说:

  • UI 线程通常负责遍历 View 树、运行自定义 onDraw()、记录绘制命令。
  • RenderThread 负责消费绘制命令并准备 GPU 工作。
  • GPU 异步执行真正的图形计算。

Android 官方也将硬件加速 UI 渲染分成:

  1. UI Thread 上的 Record View#draw
  2. RenderThread 上的 DrawFrame

参见 Android Slow Rendering

因此,即使启用了硬件加速,下面这些代码仍然会卡 UI 线程:

override fun onDraw(canvas: Canvas) {
    // 复杂业务计算
    // 创建大量对象
    // 解码图片
    // 遍历大量数据
}

GPU 只能帮你执行图形工作,不能替你运行这些 Java/Kotlin 业务逻辑。


四、Surface、BufferQueue、GraphicBuffer 到底是什么关系

这是理解 Android 图形系统的核心。

GraphicBuffer:一张可流转的画布

可以把它理解为一块保存像素数据的图形缓冲区。

它可能被不同硬件访问:

  • CPU 写入;
  • GPU 渲染;
  • SurfaceFlinger/GPU 读取并合成;
  • HWC/DPU 扫描输出。

但严格来说,并不是“GraphicBuffer 一定由 GPU 分配”。Buffer 通常通过 Gralloc/allocator 体系分配,根据 usage、格式和设备能力,得到 CPU、GPU、显示硬件可访问的内存。

Surface:生产者入口

对于应用来说,Surface 更像一块可以提交图像的输出端口。它代表 BufferQueue 的生产者一侧,而不是“永远固定的一块显存”。

生产者通常执行:

dequeueBuffer
→ 获得一个可写 Buffer
→ 渲染
→ queueBuffer

Java SurfaceANativeWindow、EGL window surface,最终都围绕这个生产过程工作。

BufferQueue:生产者和消费者之间的传送带

典型角色是:

生产者:App / RenderThread / 视频解码器 / Camera
消费者:SurfaceFlinger / SurfaceTexture / ImageReader

经典状态循环:

FREE
  ↓ dequeueBuffer
DEQUEUED
  ↓ queueBuffer
QUEUED
  ↓ acquireBuffer
ACQUIRED
  ↓ releaseBuffer
FREE

需要注意:

SurfaceFlinger 是常见消费者,但并非所有 BufferQueue 的消费者都是 SurfaceFlinger。

例如:

  • Camera → SurfaceTexture:App 是消费者;
  • 解码器 → SurfaceView:SurfaceFlinger 是消费者;
  • Camera → ImageReader:ImageReader 是消费者。

BufferQueue 是图形数据流转的通用管道,而不仅是“App 到 SurfaceFlinger”的专用队列。AOSP BufferQueue and Gralloc


五、Fence 不是普通互斥锁

原文把 Fence 类比成硬件互斥锁,便于入门,但容易让人形成错误模型。

更准确地说:

Fence 是一张“此前提交的异步工作是否已经完成”的凭证。

比如 GPU 渲染是异步的:

CPU:我已经把渲染命令提交给 GPU 了
GPU:但我还没画完
CPU:先把 Buffer 连同 acquire fence 一起交出去
消费者:等 fence signal 后再读取

Fence 的价值是避免 CPU 阻塞等待,同时确保下游不会过早读取尚未完成的 Buffer。

典型有两类:

  • Acquire fence:消费者什么时候可以开始读取。
  • Release fence:生产者什么时候可以重新使用这个 Buffer。

它表达的是异步任务之间的依赖关系,不一定意味着“某时刻只能有一个角色碰这块内存”。官方同步框架说明见 AOSP Synchronization Framework


六、SurfaceFlinger 真正负责什么

SurfaceFlinger 是系统级合成器,主要工作不是替 App 绘制按钮文字,而是:

  1. 收集各个 Surface 提交的新 Buffer;
  2. 维护与窗口对应的 Layer;
  3. 计算 Layer 的 Z 顺序、裁剪、变换、透明度、可见区域;
  4. 把 Layer 列表交给 HWC 评估;
  5. 对需要 CLIENT composition 的 Layer 做 GPU 合成;
  6. 把最终显示配置提交给 HWC;
  7. 跟踪 present fence 和显示时序。

一个 Surface 通常对应 SurfaceFlinger 侧的一个 Layer,但实际系统比“一窗口一 Layer”更复杂,可能包含容器层、颜色层、镜像层、SurfaceControl 层级等。


七、HWC、DPU、GPU 如何分工

SurfaceFlinger 不会武断地决定所有 Layer 都由 GPU 合成,它会把当前 Layer 列表交给 Hardware Composer HAL。

HWC 根据显示硬件能力,决定每层使用:

  • DEVICE composition:显示控制器/DPU/overlay plane 直接处理;
  • CLIENT composition:SurfaceFlinger 先用 GPU 合成到 client target,再交给 HWC;
  • 某些版本还存在 SOLID_COLOR、CURSOR、SIDEBAND 等类型。

可以把协商过程理解为:

SurfaceFlinger:
这里有壁纸、App、视频、状态栏、导航栏。

HWC:
视频和状态栏我可以用硬件平面直接显示;
其余几个请你先用 GPU 合成到一个 client target。

SurfaceFlinger:
好,我把 GPU 合成结果和剩余硬件层一起提交给你。

这里有个重要修正:

“合成”不一定真的生成一张包含所有 Layer 的新大图。

如果 DPU 使用多个 overlay plane,显示控制器可以在扫描输出时直接组合多个 Layer,并不需要事先写出一张完整的合成 Buffer。只有 GPU CLIENT composition 部分通常需要产生一个 client target。

HWC 的职责是抽象不同厂商的显示硬件能力,并不仅仅等于 DPU 本身。AOSP Hardware Composer HAL


八、VSync 怎样让流水线协同

在经典模型中,可以想象存在三个相位:

App VSync
    App 开始处理输入、动画并生成下一帧

SF VSync
    SurfaceFlinger 开始为即将显示的帧做合成

HW VSync
    显示设备进入下一次刷新,呈现已准备好的内容

这些信号周期相同,但相位可以不同。

这意味着流水线可能同时处理不同帧:

屏幕正在显示 N
SurfaceFlinger 正在合成 N+1
App 正在生成 N+2

因此“App 渲染完,下一瞬间屏幕就显示”通常是不成立的,中间还涉及:

  • Buffer 入队;
  • SurfaceFlinger latch;
  • 合成;
  • HWC present;
  • 显示扫描输出。

官方将 VSync 的目标描述为同步 App 渲染、SurfaceFlinger 合成和屏幕刷新。AOSP VSync

在可变刷新率、FrameTimeline、预测 VSync 和现代调度策略下,真实实现已经比 Android 7 的 DispSync 模型复杂得多,但这套流水线思维仍然有效。


九、双缓冲和三缓冲到底解决什么

如果只有一个 Buffer:

屏幕正在读它
App 就不能同时改它

双缓冲允许:

屏幕读取 A
App 渲染 B
下次交换 A/B

但如果生产者或消费者偶尔来不及,就可能互相等待。三缓冲增加一个周转 Buffer,可以降低流水线停顿概率,让 App、GPU、SurfaceFlinger/显示硬件更容易并行。

代价是:

  • 增加图形内存;
  • 队列积压时可能增加一帧延迟。

因此三缓冲不是“帧率加倍”,它解决的是流水线并发和抖动容忍度问题。


十、卡顿究竟发生在哪里

不要把所有卡顿都归结为“主线程超过 16.6ms”。

一帧可能在多个阶段失约:

UI Thread
  输入/动画/measure/layout/draw 太慢
        ↓
RenderThread
  构建帧、纹理上传、复杂路径处理太慢
        ↓
GPU
  overdraw、shader、像素填充量过大
        ↓
BufferQueue
  没有可用 Buffer,生产者阻塞
        ↓
SurfaceFlinger
  Layer 太多、合成复杂
        ↓
HWC / Display
  present 或 fence 延迟

刷新率对应的理论周期:

刷新率单周期
60 Hz16.67 ms
90 Hz11.11 ms
120 Hz8.33 ms

但不能简单理解成“App 独占整个周期”。App、RenderThread、GPU 和 SurfaceFlinger 有各自的 deadline,而且是流水执行的。

诊断现代 Android 卡顿时,优先使用:

  • Perfetto 的 FrameTimeline;
  • dumpsys gfxinfo
  • Android Studio System Trace;
  • Profile HWUI Rendering;
  • SurfaceFlinger Layer/transaction trace。

十一、SurfaceView 为什么特殊

普通 View 一般画进 Activity 主窗口对应的 Surface。

SurfaceView 则拥有独立 Surface:

Activity 主 Surface
├── 普通 View 内容
└── SurfaceView 只负责布局/占位等

SurfaceView 独立 Surface
└── 视频、Camera、游戏画面

两个 Surface 在 SurfaceFlinger 中作为不同 Layer 合成。

优点:

  • 可以由独立线程持续生产帧;
  • 视频解码器、Camera、OpenGL/Vulkan 可以直接输出;
  • 有机会使用硬件 overlay,减少额外拷贝和 GPU 合成;
  • 不必把视频每帧重新画进 View 树。

但“SurfaceView 不需要 VSync”这个说法不够准确。它可以自主生产和提交 Buffer,不依赖普通 View traversal;但若希望平滑、低延迟、无抖动,生产节奏仍然应该与显示时序协调。

与之对比,TextureView 的内容作为纹理进入 App 窗口的渲染体系,变换更灵活,但往往多一次纹理合成,性能和功耗未必优于 SurfaceView。

十二、最终记忆框架

只需记住下面五句话:

  1. View 系统决定“画什么、画在哪里”,Surface 提供“把一帧交出去”的通道。
  2. GraphicBuffer 是帧数据,BufferQueue 是帧数据的传送带,Fence 是异步任务的交接凭证。
  3. App/RenderThread/GPU 生产单个 Surface 的内容,SurfaceFlinger 管理整个系统的 Layer。
  4. HWC 在 GPU 合成和显示硬件合成之间分配工作,DPU 最终负责显示侧处理。
  5. VSync 不负责绘图,它负责给 App、SurfaceFlinger 和显示设备提供统一的时间节拍。

主要链路:

invalidate/requestLayout
→ Choreographer
→ ViewRootImpl traversal
→ DisplayList/RenderNode
→ RenderThread/GPU
→ GraphicBuffer
→ queueBuffer
→ SurfaceFlinger latch
→ HWC composition decision
→ GPU client composition 或 DPU device composition
→ present
→ 屏幕扫描显示

这就是 Android 渲染机制的主干;源码中的绝大多数类和方法,本质上都只是在完成这条链路中的某一次调度、一次内存流转或一次同步。