MediaCodec 全链路深度剖析(六):GraphicBuffer 和 BufferQueue
系列导读:第五篇结尾留了一句话——“vendor 解码出来的那块 YUV 内存,物理上一开始就分配在 SurfaceFlinger 的 BufferQueue 里”。这句话里压了两层底层概念:那块”内存”长什么样,那条”BufferQueue”怎么搬运它。这一篇把这两层拆开讲,分两大部分:
- GraphicBuffer——Android 图形栈的通用容器,从字段、usage、底层 dma-buf、Gralloc 演进到跨进程语义,一次过完。
- BufferQueue——所有图形数据流共用的管道,从抽象模型、slot 状态机、典型路径到 fence 同步,逐层下钻。
把这两层吃透,第七篇 Surface / SurfaceFlinger / HWC 才有底座。
GraphicBuffer
GraphicBuffer 是整个 Android 图形世界的通用货币:
- MediaCodec 解码输出 → GraphicBuffer
- GPU 渲染目标 → GraphicBuffer
- SurfaceFlinger 合成输入 → GraphicBuffer
- HWC 扫描输出 → GraphicBuffer
没有它就没有零拷贝。
字段构成:四层包装一根 fd
class GraphicBuffer : public ANativeWindowBuffer, public RefBase { |
第五篇里反复提到的”C2Block持有的native_handle“和这里的native_handle是同一个东西——Codec2 那一侧把它装在C2Buffer里看,图形这一侧把它装在GraphicBuffer里看,两边只是不同的视图,物理内存共用。
usage:分配时就确定访问权限
GraphicBuffer分配前必须指定 usage。它决定这块内存分配在哪个 heap、谁能访问、谁不能。常见 flag:
| Flag | 含义 |
|---|---|
GRALLOC_USAGE_SW_READ_OFTEN / RARELY |
CPU 读(对性能有要求) |
GRALLOC_USAGE_SW_WRITE_OFTEN / RARELY |
CPU 写 |
GRALLOC_USAGE_HW_TEXTURE |
GPU 作为纹理采样 |
GRALLOC_USAGE_HW_RENDER |
GPU 作为渲染目标 |
GRALLOC_USAGE_HW_VIDEO_ENCODER |
VPU 读(编码器输入) |
GRALLOC_USAGE_HW_COMPOSER |
HWC 可扫描 |
GRALLOC_USAGE_PROTECTED |
DRM 保护内存(CPU 不可读) |
usage 是分配前对消费者列表的预声明。Gralloc 拿到 usage 后会反查这种组合的访问者需要什么样的内存——是不是物理连续、要不要 IOMMU 映射、是不是走 secure heap。错误的 usage 会直接导致 GPU 采样失败、HWC 拒绝扫描,或者 secure 路径下解码出全黑帧。
三种最常见的组合:
// MediaCodec 解码输出(送 GL 做纹理 + 送 HWC 显示) |
第一种就是第五篇mChannel->setSurface(surface)那一步背后会用到的 usage——既要给 GL/HWC 合成,又要让 VPU 直接写进来。
底层:从 ION 到 DMA-BUF
Gralloc只是分配壳子,真正出内存的人在底下:
flowchart TB
APP["用户态
new GraphicBuffer(...)"]
GR["Gralloc HAL
allocate(width, height, format, usage)"]
SEL{"usage
是否带 PROTECTED?"}
SH["secure-heap"]
NH["system-heap
(普通 DMA 内存)"]
IOCTL["ioctl(heap_fd,
DMA_HEAP_IOCTL_ALLOC)"]
DMA[("内核 struct dma_buf")]
FD["返回 dma-buf fd"]
HANDLE["native_handle_create
(打包 fd + 元数据)"]
APP --> GR --> SEL
SEL -- 是 --> SH --> IOCTL
SEL -- 否 --> NH --> IOCTL
IOCTL --> DMA --> FD --> HANDLE
两套底层分配器先后登场过:
- ION(Android 12 之前的主流):Android 最早的 DMA 内存分配器,提供 system / contig / carveout / secure 几个 heap,支持跨进程共享和 IOMMU 映射。
- DMA-BUF heaps(Android 12+ 标准):Linux 内核主线的统一 DMA 机制,ION 已经从内核主线移除,新设备一律走 DMA-BUF。
用户态接口看起来很像(都是 ioctl + fd),但内核侧实现完全不同。简化后的 Gralloc allocate 逻辑:
int allocate(width, height, format, usage) { |
usage 在这一步决定开哪个 heap——secure 内容必须走 secure heap(CPU 不可见),普通帧走 system heap。这是 usage 标志被真实使用的地方。
跨进程:fd 数值不同,物理内存同一块
GraphicBuffer跨进程时传的不是 C++ 对象,而是里面的native_handle_t。Binder 走 fd 时内核会在目标进程里复制出新 fd——fd 数值变了,但内核里指向的是同一个struct dma_buf,物理内存同一块。
flowchart LR
subgraph P1["App 进程"]
A["GraphicBuffer
handle->data[0] = 42"]
end
subgraph P2["vendor 进程"]
B["GraphicBuffer
handle->data[0] = 78"]
end
DMA[("内核 dma_buf 对象
(同一块物理内存)")]
A -. Binder dup fd .-> B
A --> DMA
B --> DMA
对应 Parcel 接口:
// 发送端 |
这就是 Android 图形栈零拷贝的物理底座。fd 是门票,门票编号可以无数次复制,但门内的物理内存只此一份。第五篇里讲的C2Buffer跨进程、BufferPool跨进程引用计数,根上都是这一套机制。
BufferQueue 所有图形数据流共用的管道
知道了”容器”长什么样,下一个问题是——这些容器如何从一个角色流到下一个?答案是 BufferQueue。
所有 Android 图形数据流动都经过 BufferQueue:
- Camera 出帧 → BufferQueue → App
- GL 渲染 → BufferQueue → SurfaceFlinger
- MediaCodec 解码 → BufferQueue → SurfaceFlinger
- GL 渲染 → BufferQueue → MediaCodec 编码(Input Surface)
- SurfaceFlinger 合成 → BufferQueue → DPU
吃透 BufferQueue,后面所有图形问题都不慌。
抽象模型:两个角色 + 一组 slot
BufferQueue 的抽象只有两个角色——Producer(写数据的)和 Consumer(读数据的)——中间夹着一组GraphicBuffer槽位:
flowchart LR
subgraph Producer
P1["GL / MediaCodec / Camera"]
end
subgraph BufferQueue
SLOTS["Slots[MAX_BUFFERS=64]"]
QUEUE["mQueue (FIFO)"]
end
subgraph Consumer
C1["SurfaceFlinger /
ImageReader /
SurfaceTexture"]
end
P1 -- "dequeueBuffer / queueBuffer" --> SLOTS
SLOTS --> QUEUE
QUEUE -- "acquireBuffer / releaseBuffer" --> C1
接口本身非常对称:
// 生产者侧 |
Producer 走”申请-填充-提交”,Consumer 走”获取-使用-释放”,两条路在同一组 slot 上交替转手。
Slot 状态机
每个 slot 当下处于一个明确状态。一帧的”一生”就是在这个状态机上转一圈:
stateDiagram-v2
[*] --> FREE
FREE --> DEQUEUED: dequeueBuffer
(producer 申请)
DEQUEUED --> QUEUED: queueBuffer
(producer 提交)
DEQUEUED --> FREE: cancelBuffer
(producer 放弃)
QUEUED --> ACQUIRED: acquireBuffer
(consumer 拿走)
ACQUIRED --> FREE: releaseBuffer
(consumer 释放)
四个状态归属:
| 状态 | 含义 | 持有者 |
|---|---|---|
| FREE | 空闲 | BufferQueue |
| DEQUEUED | 生产者申请到,待填充 | Producer |
| QUEUED | 已提交,等待消费 | BufferQueue 内部队列 |
| ACQUIRED | 消费者持有中 | Consumer |
整张图压成一个核心约束:一块 slot 同时只能有一个所有者。Producer 申请它就脱离 BufferQueue 掌控,提交回去 BufferQueue 又拿到所有权,Consumer 拉走再脱离一次。两端永远不会同时改写同一块物理内存。
底下的数据结构相当朴素:
class BufferQueueCore { |
每个 slot 持有一个GraphicBuffer和它的状态。mQueue是 FIFO 列表,记的是已经 QUEUED 但还没被 Consumer 拉走的 item——Consumer 调acquireBuffer按顺序从这里出队。
一帧从 MediaCodec 到 SurfaceFlinger 的实际走法
把第四、第五篇的输出路径接进来,对照接口看一次端到端时序:
sequenceDiagram
participant V as Vendor (VPU)
participant P as Producer 端
(CCodecBufferChannel)
participant BQ as BufferQueue
participant SF as Consumer 端
(SurfaceFlinger)
participant G as GPU
V->>V: 解码一帧 YUV
(目标内存来自 BUFFERQUEUE pool)
V->>V: 触发 acquireFence
P->>BQ: dequeueBuffer(&slot, &fence, ...)
P->>BQ: queueBuffer(slot, QueueBufferInput{
timestamp, dataSpace, transform,
acquireFence, hdrMetadata})
BQ->>BQ: slot 转 QUEUED
BQ->>SF: 唤醒 Consumer (onFrameAvailable)
SF->>BQ: acquireBuffer(&item)
BQ->>SF: 返回 BufferItem (含 acquireFence)
SF->>G: 合成命令 + wait(acquireFence)
G->>G: 等 fence signal 后采样 buffer
G->>SF: 触发 releaseFence
SF->>BQ: releaseBuffer(slot, releaseFence)
BQ->>P: slot 回到 FREE (供下一次 dequeue 等 releaseFence)
生产者侧实际写法:
// 1. 申请空闲 slot(同步模式没空 slot 会阻塞) |
这就是第五篇结尾说的”BufferChannelrenderOutputBuffer把它queueBuffer给 BufferQueue 时 Consumer 直接 latch”——对应的接口就是这几行。这块 slot 的物理内存早在 vendor 解码时已经写完,App 进程的renderOutputBuffer没有任何拷贝,只在协议层走一遍queueBuffer。
消费者侧(SurfaceFlinger 把这帧 latch 到 Layer 上):
BufferQueue::BufferItem item; |
releaseFence传回去后,Producer 下一轮要复写这块 slot 时会等这个 fence。整把循环就接上了。
fence:流水线全程零 CPU 等待
BufferQueue 接口里反复出现两个 fence:
- Acquire Fence(Producer → Consumer):填充完成信号。Consumer 用前要 wait。
- Release Fence(Consumer → Producer):消费完成信号。Producer 复写前要 wait。
两个 fence 加起来构成一帧 buffer 在两端之间的完整同步握手:
sequenceDiagram
participant P as Producer (VPU)
participant BQ as BufferQueue
participant C as Consumer (GPU 合成)
P->>BQ: queueBuffer(slot, acquireFence)
Note over P: acquireFence 由 VPU 解码完成触发
BQ->>C: acquireBuffer(item) 含 acquireFence
C->>C: GPU 命令流插入 wait(acquireFence)
C->>C: GPU 异步渲染
C->>BQ: releaseBuffer(slot, releaseFence)
Note over C: releaseFence 由 GPU 合成完成触发
BQ->>P: 下次 dequeueBuffer 返回 releaseFence
P->>P: VPU 入队前 wait(releaseFence)
整张图的关键是没有任何一处 CPU 主动wait()阻塞。Producer 提交 acquireFence 后立刻返回,Consumer 拿到 fence 不是马上等,而是把 wait 操作插进 GPU 命令流——GPU 内部排队等 VPU 信号,VPU 那边硬件触发 fence 时 GPU 命令流自然解锁。Release 方向也一样,VPU 收到 releaseFence 是丢给硬件去等,CPU 这条线一刻不停。
硬件流水线全程零 CPU 等待的本质在这里。BufferQueue 不光是数据通道,也是 fence 的中转站——所有跨硬件的时序契约都借queueBuffer / acquireBuffer / releaseBuffer这三个接口顺手传递。
QueueBufferInput:一次 queue 携带的全部信息
最后看queueBuffer这一次调用到底带了什么。QueueBufferInput几乎把一帧的所有元信息都囊括了:
struct QueueBufferInput { |
把这些字段对应到第四、第五篇的链路:
timestamp来自C2Work的input_ordinal.timestamp,一路 forward 过来dataSpace是 vendor 解码时根据 SPS/VUI 推出来的色彩空间transform对应MediaFormat里的KEY_ROTATIONfence是上一节那条 acquire fencehdrMetadata在 Gralloc 4.0 之前必须走的 side channel,4.0 之后可以走GraphicBuffer元数据通道,接口本身保留兼容
CCodecBufferChannel::renderOutputBuffer做的事,就是把这些字段从 Codec2 的C2Buffer + C2Info体系翻译过来,组装成一个QueueBufferInput扔过去。SurfaceFlinger 拿到之后,把这些信息一路传给 RenderEngine 或 HWC——颜色空间转换、HDR tonemap、旋转/裁剪全靠这一坨字段。
总结
这一篇按”容器 + 管道”两层把 MediaCodec 输出端拆完:
GraphicBuffer 部分:
- 它是 Android 图形栈的通用容器,里面包元数据,外面套引用计数,最内层是一根
native_handle_t里的 dma-buf fd。MediaCodec / GPU / SurfaceFlinger / HWC 全都只认它。 - usage flag 是分配前对访问者的预声明——决定走哪个 heap、能不能 CPU 读、能不能 HWC 扫描,写错直接报废。MediaCodec 输出走的是
HW_TEXTURE | HW_COMPOSER。 - 底层从 ION 切到 DMA-BUF 是一次平滑替换。用户态接口几乎没变,内核换了实现。Gralloc 4.0 之后元数据并入 buffer 本身,HDR 元数据不再需要单独 side channel。
- 跨进程靠 fd dup——fd 数值各进程不同,但内核里指向同一个
dma_buf,物理内存同一块。这是零拷贝的物理底座。
BufferQueue 部分:
- 它是所有图形数据流的共用管道。Camera / GL / MediaCodec / SurfaceFlinger 互通走的都是它。
- slot 在 FREE / DEQUEUED / QUEUED / ACQUIRED 四态间轮转,一块物理内存同时只有一个所有者。
- 同步模式保不丢帧但可能阻塞;异步模式保不阻塞但丢最旧那帧。MediaCodec Output 默认开足 5-12 块 slot 喂硬件 VPU pipeline。
- acquireFence / releaseFence 把零 CPU 等待做成机制——Producer/Consumer 之间所有跨硬件的时序契约都靠 fence 传递,CPU 全程只做转手,等待发生在 GPU/VPU 命令流内部。
回到第五篇结尾那句话——vendor 解码出来的 YUV”物理上一开始就分配在 SurfaceFlinger 的 BufferQueue 里”——展开成一句更完整的话:
vendor 进程通过BUFFERQUEUE类型的C2BlockPool,向 SurfaceFlinger 的 BufferQueue 调一次dequeueBuffer,拿到一块 slot 对应的GraphicBuffer;这块GraphicBuffer底层是一根 dma-buf fd,vendor 那边的C2Buffer、App 这边的GraphicBuffer、SurfaceFlinger 那边的GraphicBuffer,是同一块物理内存在三个进程里的三个视图。VPU 解码完成时挂一根 acquire fence,App 这边只走协议把 slot 和 fence 移交一下,SurfaceFlinger 拿到后用 fence 安排 GPU 等硬件信号——整条链上没人拷过一字节。