18、匿名共享内存(Ashmem):Ashmem的使用场景,MemoryFile API,跨进程内存共享
匿名共享内存,英文叫 Ashmem(Anonymous Shared Memory)。
说实话,我第一次接触 Android 内存管理时,觉得 Ashmem 是个挺神奇的东西。它既不像堆内存那样随用随取,也不像文件 I/O 那样慢吞吞。它更像是一块「飞地」—— 多个进程都能看到它,但谁也不会真正「拥有」它。
嗯,咱们今天就来聊聊这个。
18.1 Ashmem 是什么?
Ashmem 是 Android 内核提供的一种共享内存机制。它允许不同进程访问同一块物理内存,而且这块内存是匿名的 —— 也就是说,它没有关联到任何文件系统路径。
你可能会问:「那它跟普通的共享内存有什么区别?」
区别大了。Ashmem 有一个关键特性:它可以被内核回收。当系统内存紧张时,内核有权把 Ashmem 占用的内存换出(swap out),甚至直接回收。这一点,普通的 POSIX 共享内存做不到。
我个人觉得,Ashmem 的设计哲学就是:「我给你共享的能力,但我也保留收回的权利。」这在移动设备上非常实用 —— 毕竟手机的内存就那么点,不能让你一个进程占着茅坑不拉屎。
核心要点: Ashmem 是内核驱动的匿名共享内存,支持内存回收,适合跨进程传递大数据。
18.2 Ashmem 的使用场景
在实际项目中,Ashmem 最常见的用途有三个:
- 跨进程传递大块数据 —— 比如图片、视频帧、序列化对象。用 Binder 传超过 1MB 的数据会崩,但 Ashmem 没这个限制。
- 共享只读资源 —— 比如字体文件、配置文件、预加载的图标。多个进程读同一块内存,省空间。
- SurfaceFlinger 与应用进程的图形数据传递 —— 这个比较底层,但确实是 Ashmem 的经典用法。
我记得有一次做即时通讯 App,需要把一张 5MB 的图片从服务进程传到 UI 进程。用 Binder 直接传?不行,TransactionTooLargeException 直接教你做人。后来改用 Ashmem,问题就解决了。
小提示: Binder 传输数据的上限通常是 1MB(具体看内核配置)。超过这个量,请考虑 Ashmem 或 ContentProvider 的 FileProvider。
18.3 MemoryFile API
Android 为 Ashmem 封装了一个上层 API,叫 MemoryFile。它在 android.os 包下,使用起来比直接操作文件描述符友好得多。
说白了,MemoryFile 就是 Ashmem 的 Java 层代言人。你不需要关心 ioctl 调用,也不需要处理 native 层的文件描述符。它给你一个类似文件的接口,读写都像操作内存一样快。
18.3.1 基本用法
来看一段代码。这是我当年写的一个工具类,用来在进程间传递图片数据:
// 写入端(进程 A)
public void writeToAshmem(byte[] data, String name) {
try {
MemoryFile memoryFile = new MemoryFile(name, data.length);
memoryFile.writeBytes(data, 0, 0, data.length);
// 获取文件描述符,通过 Binder 传给另一个进程
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
FileDescriptor fd = (FileDescriptor) method.invoke(memoryFile);
// 把 fd 通过 Binder 传过去
// ... 省略 Binder 调用代码
} catch (Exception e) {
e.printStackTrace();
}
}
// 读取端(进程 B)
public byte[] readFromAshmem(FileDescriptor fd, int size) {
try {
MemoryFile memoryFile = new MemoryFile("shared", size);
// 注意:这里需要用反射或者 ParcelFileDescriptor 来关联 fd
// 实际项目中建议用 ParcelFileDescriptor 来传递
byte[] buffer = new byte[size];
memoryFile.readBytes(buffer, 0, 0, size);
return buffer;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
嗯,这里有个坑。你看 getFileDescriptor() 这个方法,它是隐藏的(@hide),需要用反射才能调用。为什么 Google 要把它藏起来?我猜是不想让开发者滥用 Ashmem,毕竟它涉及跨进程资源管理,用不好容易出问题。
注意: MemoryFile 的 getFileDescriptor() 是隐藏 API,在 Android 9 之后可能被限制。建议使用 ParcelFileDescriptor 来传递文件描述符。
18.3.2 MemoryFile 的优缺点
| 优点 | 缺点 |
|---|---|
| API 简单,像操作文件一样方便 | 隐藏 API 多,需要反射 |
| 支持内存回收,系统压力大时自动释放 | 只能用于匿名共享内存,不能映射到文件 |
| 跨进程传递文件描述符即可共享 | 读写需要同步,不支持并发 |
18.4 跨进程内存共享的实现
跨进程共享 Ashmem,核心就两步:
- 进程 A 创建 Ashmem,拿到文件描述符(fd)。
- 进程 A 把 fd 通过 Binder 传给进程 B,进程 B 用 fd 映射同一块内存。
你想想看,这其实跟父子进程之间共享文件描述符是一个道理。只不过 Ashmem 的 fd 是「匿名」的,不指向任何磁盘文件。
我曾经在做一个多媒体项目时,需要把摄像头采集的帧数据从 CameraService 传到 App 进程。数据量很大,每帧 1080p 的 YUV 数据接近 3MB。用 Binder 传?想都别想。后来就是用 Ashmem + Binder 传递 fd 的方式搞定的。
具体做法是:
- CameraService 创建 Ashmem,写入帧数据。
- 通过 Binder 把 fd 传给 App 进程。
- App 进程用
ParcelFileDescriptor接收,然后映射成MemoryFile读取。
整个过程不需要拷贝数据,零拷贝!这就是 Ashmem 的魅力所在。
18.5 知识体系图
下面这张图,是我梳理的 Ashmem 核心知识结构。你可以把它当作一张「地图」,方便以后回顾。
18.6 避坑指南
最后,分享几个我踩过的坑:
- 不要忘记 close() —— MemoryFile 用完后一定要调用
close(),否则文件描述符会泄漏。我曾经在循环中创建 MemoryFile 忘记关闭,结果 /proc/self/fd 下堆了几百个 ashmem 文件描述符,系统直接报 "Too many open files"。 - 注意线程安全 —— MemoryFile 不是线程安全的。多线程读写同一块 Ashmem 时,需要自己加锁。我建议用
synchronized或者ReentrantLock保护。 - Android 10 之后的变化 —— 从 Android 10(API 29)开始,Google 对隐藏 API 的限制更严格了。如果你还在用反射调用
getFileDescriptor(),建议换成ParcelFileDescriptor的方案。
我的习惯: 在项目里封装一个 AshmemHelper 工具类,统一管理创建、写入、读取、关闭的逻辑。这样即使底层 API 变了,也只需要改一个地方。
好了,关于 Ashmem 就聊这么多。它虽然不像 Binder 那样天天被提起,但在处理大块数据跨进程传递时,真的是不可或缺的利器。
公众号:蓝海资料掘金营,微信deep3321