Android SharedMemory 详细使用

Android SharedMemory 详细使用总结

注意:android.os.SharedMemory Android 8.0 (API26) 新增 ,属于系统提供的匿名共享内存,跨进程共享一块物理内存;区别于老的 MemoryFile,也区别于 MMAP 文件、Binder、Socket。车载开发经常用来做大字节流跨进程传输(图片、二进制帧、CAN 大数据),避免 Binder 传输大数据触发 TransactionTooLarge 异常。

核心概念

  1. SharedMemory :一块匿名共享内存,没有磁盘文件,在内核,通过 fd 文件描述符跨进程传递。
  2. 本进程:创建 -> 获取 fd -> map 映射到本进程虚拟地址,读写。
  3. 跨进程:把 fd 通过 Binder 传给另外进程,对方拿到 fd 再 map 映射,同一块物理内存,两个进程虚拟地址不一样,物理是同一块
  4. 内存本身没有同步锁 !SharedMemory 只负责内存共享,多进程读写需要自己加锁(如 Futex、文件锁),否则会出现竞态脏数据
  5. 不再使用必须关闭 fd、unmap,否则内存泄漏。

重要:TransactionTooLargeException Binder 最大传输限制约 1MB,超过优先用 SharedMemory。

关键 API

表格

API 作用
SharedMemory.create(name, size) 创建一块匿名共享内存,name 仅调试用,内核不做区分
getFd() 获取底层文件描述符 ParcelFileDescriptor,Binder 可以序列化传递
map(prot, offset, size) 将共享内存映射到当前进程,返回 MemoryBlock
MemoryBlock.getBuffer() 获取 ByteBuffer,直接读写这块共享内存
close() 关闭 SharedMemory,减少 fd 引用计数;所有进程全部 close,内核才释放物理内存
unmap() 解除本进程虚拟地址映射,释放本进程虚拟内存,不影响其他进程

⚠️:map()得到的 MemoryBlock,用完一定要 unmap ();SharedMemory 对象用完一定要 close ()。

完整示例代码

1)创建共享内存(进程 A:写端)

java

运行

ini 复制代码
// 1. 创建 1024字节共享内存
SharedMemory sharedMemory = SharedMemory.create("my_shm_demo", 1024);

// 2. 映射:PROT_READ | PROT_WRITE 可读可写
MemoryBlock memoryBlock = sharedMemory.map(SharedMemory.PROT_READ | SharedMemory.PROT_WRITE,0,1024);
ByteBuffer buffer = memoryBlock.getBuffer();

// 写数据
String content = "hello shm cross process";
buffer.put(content.getBytes(StandardCharsets.UTF_8));

// 3. 获取fd,用于Binder传给进程B
ParcelFileDescriptor pfd = sharedMemory.getFd();

// ⚠️ 重点:map只是虚拟映射,不要马上close!close会减少fd计数,对方还没使用不能close
// 写完之后,把pfd通过AIDL传给另外进程

2)接收方(进程 B,读端,拿到 ParcelFileDescriptor)

java

运行

ini 复制代码
// 拿到AIDL传过来的 ParcelFileDescriptor pfd
// 根据fd构建SharedMemory对象
SharedMemory readShm = SharedMemory.fromFileDescriptor(pfd);

// 映射到本进程虚拟地址空间
MemoryBlock readBlock = readShm.map(SharedMemory.PROT_READ,0,1024);
ByteBuffer readBuf = readBlock.getBuffer();

byte[] data = new byte[1024];
readBuf.get(data);
String res = new String(data, StandardCharsets.UTF_8).trim();

Log.d("SHM","读取:"+ res);

// 使用完毕释放资源
readBlock.unmap();
readShm.close();
pfd.close();

3)写端用完释放(进程 A)

java

运行

scss 复制代码
memoryBlock.unmap(); // 解除本进程映射
sharedMemory.close(); // 关闭fd

生命周期(非常关键,车载开发踩坑点)

  1. SharedMemory.create() → 内核分配物理内存,fd 引用计数 = 1
  2. Binder 传递 ParcelFileDescriptor 给 B 进程,内核 fd 引用计数 +1
  3. 只要任意一个进程没有 close (),这块物理内存不会被内核回收,发生内存泄漏
  4. unmap():只解除当前进程虚拟地址映射,不影响内核物理内存、不改变 fd 引用计数。

误区:调用 unmap 不等于释放共享内存!必须全部进程调用 close () 才释放物理内存。

AIDL 传递 fd 模板

AIDL 接口,直接使用ParcelFileDescriptor,系统会自动序列化 fd 跨进程:

aidl

csharp 复制代码
// IShmService.aidl
interface IShmService {
    void sendShm(in ParcelFileDescriptor pfd, int shmSize);
}

常见坑总结

坑 1:缺少同步,多进程并发读写数据错乱

SharedMemory本身不带锁

  • 方案 1:共享内存头部预留 4 字节,放 futex 锁;
  • 方案 2:Binder 传递信号,写完再通知对方读;

❌ 不要直接两边同时读写,读到半更新脏数据。

坑 2:map 之后直接 close SharedMemory

java

运行

ini 复制代码
SharedMemory shm = SharedMemory.create("test",1024);
MemoryBlock block = shm.map(PROT_READ|PROT_WRITE,0,1024);
shm.close(); // ❌ 错误!本进程fd关闭,MemoryBlock的ByteBuffer还在用,会crash

正确:全部 unmap 之后,再 close。

坑 3:忘记 unmap,虚拟地址泄漏

map()会占用进程虚拟内存,反复 map 不 unmap,虚拟内存耗尽 OOM。

每一次map(),必须对应一次memoryBlock.unmap()

坑 4:API 版本限制

SharedMemory API26+;低于 26 只能用旧的MemoryFile,但 MemoryFile 在高版本 Android 被限制,车载 Android8 以下很少。

坑 5:大小越界

ByteBuffer 读写超过创建时 size,直接 crash。创建时 size 必须大于等于实际二进制数据最大长度。

坑 6:ParcelFileDescriptor 用完没有 close

跨进程传递过来的ParcelFileDescriptor,使用完成必须调用.close(),否则 fd 泄漏。

SharedMemory vs Binder vs MemoryFile

表格

方案 优点 缺点 适用场景
SharedMemory 大内存,不占用 Binder 缓冲区,速度快 需要手动同步锁,需要管理 fd 生命周期 图片、CAN 大报文、二进制数据流跨进程
Binder 简单自带同步 单包限制约 1MB,大数据抛 TransactionTooLarge 小参数、普通接口调用
MemoryFile 老版本共享内存 API 不推荐,高版本权限受限,逐步废弃 旧兼容代码

车载开发最佳实践

  1. 业务流程:A 进程填充二进制 → 创建 SharedMemory → AIDL 传递ParcelFileDescriptor + size → B 进程拿到 fd 映射读取;读完 B 主动关闭 fd;A 使用完 unmap+close。
  2. 不要把业务数据全部塞 Binder,>500KB 优先走 SharedMemory。
  3. 读写同步:建议约定:写进程写完数据,再通过 Binder 调用通知读进程去读取,避免竞态,简单业务不用搞 futex 锁。
  4. 异常流程:捕获异常,finally块做 unmap、close,防止 fd 泄漏。

finally 释放模板(推荐模板)

java

运行

ini 复制代码
SharedMemory shm = null;
MemoryBlock block = null;
try{
    shm = SharedMemory.create("demo", 2048);
    block = shm.map(SharedMemory.PROT_READ|SharedMemory.PROT_WRITE,0,2048);
    // 业务读写逻辑
}catch (Exception e){
    e.printStackTrace();
}finally {
    if(block != null){
        block.unmap();
    }
    if(shm != null){
        shm.close();
    }
}

补充:Kotlin 版本简要示例

kotlin

scss 复制代码
val shm = SharedMemory.create("kotlin_shm", 1024)
val block = shm.map(SharedMemory.PROT_READ or SharedMemory.PROT_WRITE,0,1024)
val byteBuffer = block.buffer
byteBuffer.put("test".toByteArray())
val pfd = shm.fd
// 传给另外进程
// 释放
block.unmap()
shm.close()
相关推荐
sun0077001 小时前
android的dns的常见错误resolv
android
恋猫de小郭2 小时前
Flutter A2UI 的正确用法,怎么把 AI 和动态 UI 结合有效生产
android·前端·flutter
余额瞒着我当琳2 小时前
C++--vector第二讲:手写 C++ STL:vector 源码剖析与迭代器失效分析
android·java·c++
程序员-珍2 小时前
安卓开发:同名 styles.xml 在不同分支/文件夹的合并规则
android·xml·前端
我命由我123453 小时前
Compose Codelab 学习 - Compose 中的基本布局
android·java·java-ee·kotlin·android studio·android-studio·android runtime
ii_best3 小时前
手机自动化脚本按键精灵实战:随机布局安全数字键盘的自动化输入方案
android·运维·ios·自动化·手机
2601_965798473 小时前
Boutique Business Consulting WordPress Architecture & SEO Setup
android·theme·wordpress
三少爷的鞋3 小时前
Android 架构进阶:为什么项目越大,越需要把对象创建权拿走
android
杜子麟5 小时前
Android studio模拟器离线安装
android·macos·android studio