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 驱动在传输过程中,会自动将发送进程的文件描述符转换为接收进程的文件描述符。这个过程叫做“文件描述符的跨进程迁移”。
具体流程是这样的:
- 创建 ashmem 区域:发送进程调用
ashmem_create_region()或ASharedMemory_create(),获得一个文件描述符。 - 映射到本进程地址空间:通过
mmap()将这块共享内存映射到本进程的虚拟地址空间。 - 写入数据:把大块数据拷贝到共享内存中。
- 通过 Binder 传递文件描述符:将文件描述符封装在 Binder 事务的
flat_binder_object中,类型设置为BINDER_TYPE_FD。 - 接收进程获取文件描述符:Binder 驱动自动完成文件描述符的转换,接收进程拿到的是自己进程空间中的有效文件描述符。
- 映射并读取:接收进程通过
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 结合的整体流程
为了让你更直观地理解,我画了一张流程图:
性能对比:有 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 系统设计中的一个重要思想。