Android 绘图框架:应用画、SurfaceFlinger 统筹、HWC 分配 plane
结论:Android 的一帧画面由三类进程接力完成:应用进程把窗口画进自己的 buffer,SurfaceFlinger 收齐各窗口的 buffer 后决定怎么合成,厂商的 HWC 把图层分给显示控制器的 plane 并提交给 KMS。 GPU 只在两个地方出场:应用画窗口,以及合成 HWC 处理不了的图层。其余图层直接交给显示控制器叠加,这是 Android 省电的关键。
本文以 Android 13 及以后的版本为准:buffer 走 BLASTBufferQueue,HWC 接口是 AIDL 的 composer3。
逻辑视图:应用、SurfaceFlinger、厂商 HAL 三类进程各管一段
Section titled “逻辑视图:应用、SurfaceFlinger、厂商 HAL 三类进程各管一段”应用进程负责把自己的窗口画好:
- UI 线程遍历 View 树,把要画的内容记录成显示列表,不直接调 GPU。
- RenderThread(HWUI)用 Skia 回放显示列表,生成 GLES 或 Vulkan 命令。
- 加载器(
libEGL、libvulkan)按系统属性找到 Mali DDK 的libGLES_mali.so,由它把命令交给 GPU。 - BLASTBufferQueue 管理窗口的几块 buffer:向 gralloc 申请 buffer,画完后把 buffer 和 acquire fence 打包进 Transaction 交给 SurfaceFlinger。Android 12 起这个队列放在应用进程里,以前在 SurfaceFlinger 里。
SurfaceFlinger 进程负责统筹:
- Scheduler 产生 VSYNC 节拍,分别唤醒应用(开始画下一帧)和自己(开始合成)。
- 前端接收各应用的 Transaction,在合成时刻锁定每个图层这一帧用哪块 buffer(latch)。
- CompositionEngine 把图层列表交给 HWC,问每层由谁合成。
- RenderEngine 用 Skia 和同一个 Mali DDK,把 HWC 退回的图层合成到一块 buffer(client target)。
厂商 HAL 服务进程由 SoC 厂商实现:
- gralloc allocator 按用途分配 buffer。比如同时要被 GPU 写、被显示控制器读的 buffer,要选双方都支持的格式和 modifier,内存从 dma-buf heaps 申请。
- composer3 HAL(HWC)决定图层和 plane 的对应关系,调用 KMS 的 atomic 提交,再把 fence 交回 SurfaceFlinger。
内核:kbase 驱动 Mali,dma-buf heaps 提供内存,sync_file 让 fence 跨进程传递,KMS 驱动控制显示控制器。
进程视图:一帧画面走十步,跨三个进程
Section titled “进程视图:一帧画面走十步,跨三个进程”一帧从 VSYNC 唤醒应用开始,到 buffer 回收结束:
sequenceDiagram participant SF as SurfaceFlinger participant UI as 应用 UI 线程 participant RT as 应用 RenderThread participant GPU as Mali(DDK + kbase) participant HWC as HWC participant KMS as KMS 驱动 SF->>UI: ① VSYNC-app UI->>RT: ② 同步显示列表 RT->>GPU: ③ Skia 生成命令并提交 RT->>SF: ④ Transaction:buffer + acquire fence Note over SF: ⑤ VSYNC-sf:锁定各层 buffer SF->>HWC: ⑥ validateDisplay:图层列表 HWC-->>SF: 每层的合成方式 SF->>GPU: ⑦ 有 CLIENT 层时,RenderEngine 合成 SF->>HWC: ⑧ presentDisplay HWC->>KMS: ⑨ atomic 提交,各 plane 带 IN_FENCE_FD KMS-->>HWC: present fence HWC-->>SF: present fence、各层 release fence SF-->>RT: ⑩ release fence:buffer 可以重用
- SurfaceFlinger 的 Scheduler 向应用发 VSYNC-app,Choreographer 开始这一帧的输入、动画和绘制。
- UI 线程画完显示列表,同步给 RenderThread,然后可以去处理下一件事。
- RenderThread 用 Skia 生成 GPU 命令,经 DDK 和 kbase 提交给 Mali。
- 提交后立刻把 buffer 和 acquire fence 放进 Transaction 发给 SurfaceFlinger,不等 GPU 画完。
- 到 VSYNC-sf,SurfaceFlinger 锁定每个图层这一帧要用的 buffer。
- SurfaceFlinger 把图层列表交给 HWC 验证。HWC 返回每层的合成方式:DEVICE 由硬件叠加,CLIENT 交回 GPU 合成。
- 有 CLIENT 层时,RenderEngine 用 GPU 把它们合成到 client target。
- SurfaceFlinger 通知 HWC 正式出图。
- HWC 组装 atomic 请求提交给 KMS。每个 plane 都带上对应 buffer 的 acquire fence,内核等 fence 触发后才在 VSYNC 切换画面。
- 新画面上屏后,KMS 给出 present fence,各层旧 buffer 的 release fence 经 SurfaceFlinger 回到应用,应用收到后就能重用这块 buffer。
步骤 ① 到 ④ 和 ⑤ 到 ⑨ 由两个不同的 VSYNC 驱动,二者有固定的相位差。应用在这一拍画,SurfaceFlinger 在下一拍合成,流水线化以后,两边可以各用一整帧的时间。
第二层:HWC 用 TEST_ONLY 试分配,硬件叠不下的图层才交给 GPU
Section titled “第二层:HWC 用 TEST_ONLY 试分配,硬件叠不下的图层才交给 GPU”上图第 ⑥ 步是 Android 省电的关键,下面单独展开。下图展开的是逻辑视图里的“composer3 HAL”。
- 输入:SurfaceFlinger 按 z 序给出图层列表,每层带 buffer、格式、位置、缩放、透明度。
- 试分配:HWC 按自己的策略把图层映射到 plane,用 atomic 的
TEST_ONLY问内核硬件能否接受。不能就换一种分配再试。 - DEVICE:分到 plane 的图层,buffer 直接交给显示控制器。图中的视频层是 YUV 格式,格式转换和缩放都由显示控制器完成,GPU 完全不碰它。
- CLIENT:分不到 plane 的图层交回 SurfaceFlinger,由 RenderEngine 用 GPU 合成成一块 client target,再占用一个 plane。图中 5 个图层、4 个 plane,最底下的壁纸和应用窗口被合成到一起。
- 其他变成 CLIENT 的原因:模糊、圆角等特效,或者显示控制器不支持的格式、缩放比例。
分配策略写在厂商 HWC 里,是 Android 显示调优的主要工作。plane 越多、能力越强,GPU 合成越少,越省电。
厂商要实现的部件:HWC、gralloc,加上 KMS 驱动
Section titled “厂商要实现的部件:HWC、gralloc,加上 KMS 驱动”Android 绘图框架里 Google 提供框架,厂商负责和硬件打交道的部分:
| 部件 | 厂商做什么 | 做不好会怎样 |
|---|---|---|
| HWC(composer3 HAL) | 图层到 plane 的分配策略;组装 atomic 请求;传递 fence;上报 VSYNC 和热插拔 | GPU 合成增多,耗电、掉帧 |
| gralloc(allocator + mapper) | 按用途选格式、modifier、内存类型;buffer 句柄要让 DDK 和 HWC 都能读懂 | GPU 写的 buffer 显示控制器读不了,或者被迫用不压缩的格式 |
| KMS 显示驱动 | 实现 plane、CRTC、connector 的 atomic 回调;接 vblank 中断;上报 plane 支持的格式和 modifier | HWC 的 TEST_ONLY 结果不准,画面撕裂或黑屏 |
| Mali DDK 集成 | 放好库和固件,设置系统属性,kbase 与用户态版本对齐 | GPU 驱动加载失败,见下一篇 |
HWC 也有开源参照:drm_hwcomposer 是直接建在 KMS atomic 上的通用实现,支持 composer3。它的分配策略比较简单,适合作为对照或起步版本。