跨进程交接一帧时,发送端和接收端通常各自持有 GraphicBuffer 包装对象,却可以访问同一个底层图形资源。底层分配、native handle、Binder fd、Mapper import、进程内缓存和 Fence 分属不同层次,不能都用"Buffer"指代。
以下以 Android 15 为参考版本,重点追踪 GraphicBuffer::flatten()/unflatten()、Binder 传递的 fd、GraphicBufferMapper::importBuffer() 和 Fence 的独立同步职责;不展开具体厂商 gralloc、dma-buf、HWC 或 Codec 驱动实现。
跨进程共享的最小单位是"可被接收进程导入的资源引用",而不是可以搬运的 C++ 对象。GraphicBuffer、native handle、fd 和 CPU 虚拟地址属于不同进程的本地包装或访问路径;Fence 只描述访问时间,不负责证明资源身份。资源交接因此分成身份链和时间链:flatten、Binder、unflatten、import 处理前者,Fence 处理后者。
Buffer 的五层身份
为了避免"复用的是同一个实例吗"的混淆,先把一个图形资源写成五层:
| 层次 | 典型内容 | 是否跨进程相同 |
|---|---|---|
| 底层分配 R | gralloc/驱动管理的图形资源与像素存储 | 两端希望引用同一资源 |
| native handle | 一个或多个 fd 与实现相关的整数数据 | 结构体和地址通常不同 |
GraphicBuffer 包装 |
宽高、stride、format、usage、mId、generation、handle 和访问入口 |
每个进程各自维护 |
| CPU 虚拟地址 | 进程 lock 后得到的本地映射地址 | 通常不同,甚至不存在 |
| Fence | 某批异步工作何时完成的依赖 | fd 数字可不同,但可引用同一完成依赖 |
可以把它类比成仓库、取件凭证和说明卡:
text
仓库里的真实画布 = 底层 gralloc Buffer R
取件凭证 = native handle / fd
Producer 进程的说明卡 = GraphicBuffer P
Consumer 进程的说明卡 = GraphicBuffer C
"上一批工作完成了吗"通知单 = Fence
native handle 和 GraphicBuffer 负责描述、导入和访问资源,像素存储仍由底层分配和设备访问路径管理。
图中 P、C、HP、HC 都是进程内对象或引用路径;只有它们共同指向的底层资源 R 是本次跨进程复用的核心。
指针为何不能跨进程复用
假设 Producer 进程中有一个地址为 0x12340000 的 GraphicBuffer*。这个数值只在 Producer 的虚拟地址空间里有意义。Consumer 进程中的同一个数值可能没有映射,也可能指向完全不同的对象。
更重要的是,普通 C++ 对象包含:
- 本进程的成员地址和方法调用目标;
sp<>引用计数和析构生命周期;- 当前进程的 native handle 指针;
- 只对当前进程有效的缓存和锁。
Binder 可以传输接口对象或可序列化的资源凭证。跨进程交接需要使用不依赖发送进程地址空间的描述:
text
发送进程的 GraphicBuffer*
✗ 不能直接作为接收进程的对象
GraphicBuffer 属性 + native handle 中的 fd/int
✓ 可以被接收进程重建和导入
handle 的边界
native handle 通常包含文件描述符和实现相关整数,用来让接收端定位并导入底层资源。它本身不包含像素,也不等同于全局指针或跨进程通用 ID。
fd 是当前进程文件描述符表中的编号。例如:
text
Producer 进程:fd 17 ──┐
├──> 同一底层资源 R
Consumer 进程:fd 42 ──┘
17 和 42 可以不同,因为每个进程都有自己的 fd 表;Binder 传递 fd 时,内核会在接收进程安装一个新的 fd 引用。关闭 Producer 的 fd 不等于立刻销毁 R,只要 Consumer 或其他设备路径仍持有有效引用,资源就可能继续存在。
更准确的表述是:
Binder 负责把 handle 中的 fd 引用和少量参数安全地交给接收进程;gralloc/Mapper/驱动负责让接收端把这份引用导入为当前进程可使用的图形资源。
flatten、Binder、unflatten 和 import 各自做什么
首次交接某个 Slot,或者 Slot 映射失效后重新交接时,可以按下面的时序理解:
这些动作各自处理不同对象,Binder 交接也不包含整幅像素:
| 动作 | 解决的问题 | 是否复制整幅像素 |
|---|---|---|
flatten() |
把尺寸、格式、usage、身份信息和 handle 中可传输的数据写入 Parcel | 否 |
| Binder fd 传递 | 让接收进程得到同一内核资源引用的本地 fd | 否 |
unflatten() |
从传输数据恢复接收进程的对象状态和 native handle | 否 |
importBuffer() |
让 Mapper/分配器验证、登记并返回当前进程可用的 imported handle | 否 |
| Slot 缓存 | 后续用索引定位已有本地包装,减少重复传递与 import | 否 |
AOSP GraphicBuffer::flatten() 的源码入口固定在 GraphicBuffer.cpp。就文章需要的最小模型而言,它写出 width、height、stride、format、layer count、usage、mId、generation 以及 native handle 的 fd/int。接收端随后执行相反方向的恢复,并在 Mapper 中导入。
Consumer 创建 GraphicBuffer 时建立的是本地包装,不会产生第二份像素存储:
text
第一次 dequeue / 规格变化
→ gralloc 分配底层 R
跨进程首次看到 Slot i
→ Consumer 创建 GraphicBuffer C
→ C import handle,引用 R
→ Consumer 缓存 i → C
只有第一条路径分配实际图形资源;第二条路径建立的是当前进程管理资源所需要的对象和引用。
裸 fd 不足以描述一个 GraphicBuffer
一个 fd 只是资源凭证的一部分。接收进程还需要知道:
- width、height、stride 和 layer count;
- 像素格式与 usage;
mId、generation 等身份信息;- 当前进程导入后的 handle;
- lock/unlock 或设备访问所需的生命周期;
- 本地引用计数、释放和 Slot 缓存关系。
GraphicBuffer 在进程内保存资源的几何、访问和生命周期信息:
text
GraphicBuffer C
├── 资源几何:width / height / stride / layers
├── 访问约束:format / usage
├── 资源身份:mId / generation
├── 当前进程 handle:import 后可用
├── 访问入口:CPU lock 或设备路径
└── 本地生命周期:引用、缓存、释放
不同进程分别管理本地包装和资源引用。Producer 释放 P 时,只释放自己的包装和引用;Consumer 的 C 仍可在自己的生命周期内使用。底层 R 的回收时机由相关引用、BufferQueue 生命周期和 gralloc 实现共同决定。
Slot 缓存的命中条件
BufferQueue 使用 Slot 索引作为两端缓存的定位键。典型过程是:
text
首次或缓存失效:
slot i + GraphicBuffer 属性 + handle/fd + Fence
↓
ConsumerBase::mSlots[i].mGraphicBuffer = C
规格稳定、缓存仍有效:
slot i + 帧级元数据 + Fence
↓
Consumer 直接使用本地缓存 C
这也是 BufferItem.mGraphicBuffer == nullptr 容易误读的地方。它不表示 Core 中没有 Buffer,而可能表示 Consumer 已经见过这个 Slot 映射,不需要在本次 Binder 交接中重复携带 GraphicBuffer。
Producer 侧的典型路径可以压缩为:
cpp
sp<GraphicBuffer>& gbuf(mSlots[buf].buffer);
if ((result & IGraphicBufferProducer::BUFFER_NEEDS_REALLOCATION) ||
gbuf == nullptr) {
mGraphicBufferProducer->requestBuffer(buf, &gbuf);
}
此处的 mSlots 是 Producer 侧缓存,不是 Core 的 BufferQueueCore::mSlots。两者使用相同的索引协议,但保存的包装对象和权威状态不同:Core 决定 Slot 是否已替换,Producer/Consumer 保存本地映射。
缓存会在规格或 usage 改变、Core 清理 Slot、连接重建或收到释放通知时失效。失效后需要重新取得 GraphicBuffer、重新 import 并刷新本地缓存;索引仍为 i 也不能沿用旧对象。
同一个 Slot 的第一次交接与缓存命中
用同一个 Slot i 对比两次交接,可以看出"跨进程共享"究竟省掉了什么:
| 交接时刻 | Producer 侧发送 | Consumer 侧动作 | 关键边界 |
|---|---|---|---|
第一次看到 i,或缓存失效 |
Buffer 属性、handle 中的 fd/int、帧级 Fence | unflatten() 建立本地包装,importBuffer() 登记访问路径,并缓存 i → C |
Binder 只交接描述和引用,Consumer 建立自己的包装 |
| 规格稳定且缓存有效 | Slot i 与本次帧元数据、Fence |
直接查本地 i → C,按 Fence 约束读取或合成 |
缓存只省去重复导入,Fence 仍需逐帧处理 |
| 规格/usage/连接变化 | 新的 Buffer 描述或失效通知 | 重新建立包装、import 和缓存 | 旧 imported handle 不能仅凭 Slot 索引继续使用 |
缓存命中减少描述和导入的重复工作;资源仍由各进程分别包装,Fence 也要按每一轮访问重新传递。
CPU、GPU 和跨进程不是同一件事
共享底层资源不等于共享绘制现场。Producer 写入发生在它自己的 CPU 线程、RenderThread、EGL/Vulkan context、Camera 或 Codec 执行上下文中:
text
Producer 进程提交写入任务 J,目标是 R
↓
CPU 可能继续做别的工作,GPU 在后台执行 J
↓
queueBuffer(R, acquire Fence)
↓
Consumer 通过自己的包装 C 取得 R
↓
等待 acquire Fence 后读取、合成或交给 HWC
如果使用 CPU,Producer 和 Consumer 即使都把 R 映射到自己的地址空间,得到的也通常是不同虚拟地址 VA-P 与 VA-C。如果由 GPU、HWC 或 Codec 访问,进程甚至可能没有一个可直接读写像素的 CPU 地址。
需要区分以下几组关系:
| 说法 | 是否正确 |
|---|---|
| 两个进程可以引用同一底层图形资源 | 正确 |
两个进程共享同一个 GraphicBuffer* |
跨 Binder 时不正确 |
| 两个进程的 fd 数字必须相同 | 不正确 |
| Producer 的绘制代码会迁移到 Consumer 进程 | 不正确 |
| Producer 提交的 GPU 工作可在 Binder 交接后继续执行 | 可以,Fence 负责约束读取时点 |
| BufferQueue 交接会把整幅像素复制进 Binder | 不正确 |
Fence 负责时间,不负责资源身份
handle 解决"访问哪一块资源",Fence 解决"什么时候可以安全访问"。
Producer 提交 acquire Fence 时,表示自己的写入任务可能还没有完成。Consumer acquire 到 Buffer 后,必须等这条依赖满足,才能读取或合成。Consumer release 时提交 release Fence,表示它对 Buffer 的读取、合成或硬件使用可能尚未结束;Producer 下一次取得 Slot 后要等待它,才能覆盖资源。
text
handle ─────────────────> 这是哪一块底层 Buffer
acquire Fence ──────────> Producer 写入何时完成
release Fence ──────────> Consumer 使用何时完成
Fence 本身不保存像素,也不记录 FREE/DEQUEUED/QUEUED/ACQUIRED。它通常是用户态 android::Fence 对 sync fence fd 的包装,底层完成依赖由驱动或硬件 signal。多个前置任务可以通过 Fence::merge() 合成为"全部完成后再继续"的依赖。
资源身份的完整时间线
把一次跨进程交接压缩成一条线:
text
gralloc 创建底层资源 R
↓
Producer GraphicBuffer P 绑定 handle P
↓
Producer queue:Buffer 元数据 + handle 引用 + acquire Fence
↓ Binder
Consumer unflatten:建立 GraphicBuffer C
↓
Mapper import:得到当前进程可用的 imported handle
↓
Consumer 缓存 Slot i → C
↓
等待 acquire Fence,读取/合成 R
↓
release Fence 随后续交接返回
↓
Producer 下次等待后重新写入 R
这条链同时包含两种生命周期:
- 资源生命周期: R 被分配、被多个本地 handle 引用,所有引用结束后才可能回收;
- 访问生命周期: Fence 约束一轮写入和读取的先后,下一轮可以重新使用同一 R。
两条生命周期和本地包装分别表现为:
text
"创建"可能指首次分配 R,也可能指接收端创建包装 C
"复用"可能指 Slot 继续使用,也可能指多个进程引用同一 R
"释放"可能只释放某个进程的包装,不等于立刻销毁 R
因此,诊断一条跨进程交接链可以使用三个彼此独立的问题维度:资源是否还是同一个(查 handle、mId、generation 和 import);当前进程是否能访问它(查 imported handle、lock 或设备访问路径);访问时机是否安全(查 acquire/release Fence)。这三个维度分别对应身份、映射和时间,某个维度正常并不能替代另外两个。
版本边界与验证入口
Android 15 的公共接口提供上述身份边界;不同 gralloc/Mapper 后端可能在 handle 数据、import 时机、CPU 映射和设备访问方式上存在差异。文章不把某个厂商实现写成所有设备的统一布局。
可继续核对:
GraphicBuffer.cpp:flatten/unflatten、元数据与 handle 序列化入口。GraphicBufferMapper.cpp:import 与当前进程资源访问。BufferQueueProducer.cpp:Producer 侧 Slot 与 reallocation 标记。BufferQueueConsumer.cpp:Consumer acquire 输出和 Slot 缓存失效边界。Fence.cpp:Fence fd、wait 与 merge。- Android BufferQueue and Gralloc 与 Synchronization framework。