GraphicBuffer 跨进程共享

跨进程交接一帧时,发送端和接收端通常各自持有 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 负责描述、导入和访问资源,像素存储仍由底层分配和设备访问路径管理。

flowchart LR R[底层 gralloc 分配 R<br/>真实图形资源] HP[Producer native handle<br/>fd + int] HC[Consumer imported handle<br/>fd + int] P[Producer GraphicBuffer P<br/>本地包装] C[Consumer GraphicBuffer C<br/>本地包装] VP[Producer CPU/GPU 访问路径] VC[Consumer GPU/HWC/CPU 访问路径] P --> HP --> R C --> HC --> R R --> VP R --> VC

图中 P、C、HP、HC 都是进程内对象或引用路径;只有它们共同指向的底层资源 R 是本次跨进程复用的核心。

指针为何不能跨进程复用

假设 Producer 进程中有一个地址为 0x12340000GraphicBuffer*。这个数值只在 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 映射失效后重新交接时,可以按下面的时序理解:

sequenceDiagram participant P as Producer 进程 participant B as Binder participant C as Consumer 进程 participant M as GraphicBufferMapper participant R as 底层 gralloc 资源 P->>P: GraphicBuffer P 持有 handle P P->>B: flatten 属性 + handle fd/int + Fence fd B->>C: 安装接收端 fd 引用 C->>C: unflatten,建立本地 GraphicBuffer C C->>M: importBuffer(handle C) M->>R: 登记当前进程的资源访问路径 M-->>C: imported handle / 本地包装可用 C->>C: 缓存 Slot i → GraphicBuffer C

这些动作各自处理不同对象,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-PVA-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 映射和设备访问方式上存在差异。文章不把某个厂商实现写成所有设备的统一布局。

可继续核对:

相关推荐
行业研究员1 小时前
Android内置GPS与腾讯地图定位技术对比分析
android·android定位·腾讯地图定位
Android-Flutter2 小时前
android WMS 详解(二)
android·kotlin
游戏开发爱好者83 小时前
App Store 上传 IPA 自动化,.p8 密钥认证与 CI/CD 接入实战
android·运维·ci/cd·小程序·uni-app·自动化·iphone
游戏开发爱好者84 小时前
iOS 推送怎么配置,APNs 推送证书、设备库与群发
android·ios·小程序·https·uni-app·iphone·webview
wxson728219 小时前
【Android视频监控系统技术总结】
android·音视频
hunterandroid20 小时前
[Android 从零到一] LiveData 粘性事件与单次消费:从重复触发到可靠事件总线
android
hunterandroid20 小时前
[Android 从零到一] Android 事件分发机制:从 ACTION_DOWN 到 View 事件消费链路
android
阿巴斯甜21 小时前
adb‑shell 全套命令实操总结
android