20. 匿名共享内存:Binder 如何结合 ashmem 传输大块数据?

好,咱们来聊聊一个很实际的问题。Binder 本身有 1MB 的数据大小限制,这个你肯定知道。那如果我要传一张 10MB 的图片,或者一段 50MB 的视频流,怎么办?

答案就是:Binder + ashmem(匿名共享内存)。说白了,Binder 只负责传一个“文件描述符”,真正的数据通过共享内存来传递。我当年第一次接触这个机制时,觉得这设计真巧妙——把控制流和数据流分开了。

为什么 Binder 不能直接传大块数据?

原因其实很简单。Binder 的传输机制是“拷贝一次”,从用户态 A 拷贝到内核态,再从内核态拷贝到用户态 B。如果数据量太大,这个拷贝过程会非常耗时,而且会占用大量内核缓冲区。

另外,Binder 的事务缓冲区(Transaction Buffer)在内核中是有上限的。默认情况下,单个 Binder 事务最大只能传 1MB 左右的数据。你想想看,传一张高清图片都不够用。

所以,Google 引入了 ashmem。它的核心思想是:只传引用,不传数据

ashmem 是什么?

ashmem 的全称是 Anonymous Shared Memory,匿名共享内存。它是 Android 内核提供的一种机制,允许两个进程共享同一块物理内存。

关键点在于“匿名”——它不像文件映射那样需要磁盘上的文件,而是直接分配物理内存页。两个进程通过文件描述符来访问这块内存。

我个人习惯把 ashmem 看作一个“内存快递柜”。A 进程把数据放进柜子,然后把柜子的钥匙(文件描述符)通过 Binder 传给 B 进程。B 拿到钥匙后,自己去柜子里取数据。整个过程,数据只被拷贝一次(从 A 的用户态到共享内存),B 直接读取,无需再次拷贝。

Binder 如何传递 ashmem 文件描述符?

这里有个关键机制:Binder 可以跨进程传递文件描述符。Binder 驱动在传输过程中,会自动将发送进程的文件描述符转换为接收进程的文件描述符。这个过程叫做“文件描述符的跨进程迁移”。

具体流程是这样的:

  1. 创建 ashmem 区域:发送进程调用 ashmem_create_region()ASharedMemory_create(),获得一个文件描述符。
  2. 映射到本进程地址空间:通过 mmap() 将这块共享内存映射到本进程的虚拟地址空间。
  3. 写入数据:把大块数据拷贝到共享内存中。
  4. 通过 Binder 传递文件描述符:将文件描述符封装在 Binder 事务的 flat_binder_object 中,类型设置为 BINDER_TYPE_FD
  5. 接收进程获取文件描述符:Binder 驱动自动完成文件描述符的转换,接收进程拿到的是自己进程空间中的有效文件描述符。
  6. 映射并读取:接收进程通过 mmap() 映射共享内存,直接读取数据。

核心要点:Binder 只传输了 4 字节的文件描述符,而不是 10MB 的数据。这就是效率所在。

代码示例:C++ 层实现

我曾在项目中用这种方式传输过摄像头采集的帧数据。下面是一个简化版的示例:

// 发送进程
int fd = ashmem_create_region("my_shared_mem", size);
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
memcpy(ptr, data, size);  // 写入大块数据

// 构造 Binder 事务
Parcel dataParcel;
dataParcel.writeFileDescriptor(fd);  // 写入文件描述符
// ... 调用 Binder 传输

// 接收进程
int receivedFd = reply.readFileDescriptor();
void* ptr = mmap(NULL, size, PROT_READ, MAP_SHARED, receivedFd, 0);
// 直接读取 ptr 中的数据

嗯,这里要注意一点:ashmem_create_region() 在较新版本的 Android 中已经被标记为 deprecated,推荐使用 ASharedMemory_create()(NDK 接口)。

避坑指南:我曾经踩过的坑

我曾经在做一个多媒体项目时,遇到过一个诡异的问题:接收进程读取共享内存时,数据总是有一部分是乱的。排查了半天,发现是发送进程在 Binder 传输完成之前就关闭了文件描述符。

这里有个关键点:Binder 传输文件描述符是异步的。发送进程不能假设 Binder 传输完成后才去操作共享内存。正确的做法是:

  • 发送进程在 Binder 调用返回后,不能立即释放或修改共享内存中的数据。
  • 需要等待接收进程确认已经读取完毕。
  • 或者使用同步机制(如 Binder 的同步调用)来保证时序。

警告:不要尝试在 Binder 传输后立即 munmap 共享内存。接收进程可能还没完成映射。我曾经因为这个 bug 导致系统偶尔崩溃,排查了两天才找到原因。

ashmem 与 Binder 结合的整体流程

为了让你更直观地理解,我画了一张流程图:

Binder + ashmem 传输大块数据流程图 进程 A(发送方) 进程 B(接收方) ① 创建 ashmem 区域 ashmem_create_region() ② mmap 映射到本进程 获取虚拟地址指针 ③ 写入大块数据 memcpy(ptr, data, size) ④ 通过 Binder 传递 FD writeFileDescriptor(fd) Binder 驱动 自动转换 FD FD FD ⑤ 接收文件描述符 readFileDescriptor() ⑥ mmap 映射共享内存 映射到本进程地址空间 ⑦ 直接读取数据 零拷贝读取 共享内存 物理内存页 写入 读取

性能对比:有 ashmem vs 无 ashmem

我整理了一个简单的对比表,让你直观感受一下差异:

传输方式 数据大小 拷贝次数 内核缓冲区占用 适用场景
纯 Binder ≤ 1MB 2 次(用户态→内核→用户态) 高(全部数据在内核中) 小数据、控制命令
Binder + ashmem 无限制(受物理内存限制) 1 次(用户态→共享内存) 低(仅传输 4 字节 FD) 大块数据、多媒体流

小技巧:如果你需要在 Java 层使用 ashmem,可以使用 MemoryFile 类。它封装了 ashmem 的创建和映射操作。不过要注意,MemoryFile 在 Android 10 之后有些行为变化,建议查看官方文档。

总结一下

Binder 结合 ashmem 传输大块数据,本质上就是控制流与数据流分离的设计模式。Binder 负责传控制信息(文件描述符),ashmem 负责传实际数据。这样既绕过了 Binder 的 1MB 限制,又实现了高效的零拷贝(从共享内存的角度看)。

我在实际项目中,用这个方案传输过几百 MB 的离线地图数据包,效果非常好。唯一要注意的就是生命周期管理和同步问题——别像我当年那样,在 Binder 还没传完 FD 就把共享内存给释放了。

好了,关于 ashmem 和 Binder 的结合,就聊到这里。记住一句话:传引用,不传数据,这是 Android 系统设计中的一个重要思想。


专注资料整理