详解Android Ashmem匿名共享内存
作者:michael_zhong
1. 简述
Android 的 匿名共享内存(Ashmem) 基于 Linux 的共享内存,都是在临时文件系统(tmpfs)上创建虚拟文件,再映射到不同的进程。它可以让多个进程操作同一块内存区域,并且除了物理内存限制,没有其他大小限制。相对于 Linux 的共享内存,Ashmem 对内存的管理更加精细化,并且添加了互斥锁。Java 层在使用时需要用到 MemoryFile,它封装了 native 代码。 Java 层使用匿名共享内存的4个点:
1. 通过 MemoryFile 开辟内存空间,获得 FileDescriptor;
2. 将 FileDescriptor 传递给其他进程;
3. 往共享内存写入数据;
4. 从共享内存读取数据。
下面用一个例子介绍匿名共享内存的使用,假设需要开辟一段共享内存,写入一些数据,再在另外一个进程读取这段数据。
2. 创建 MemoryFile 和 数据写入
/** * 需要写入到共享内存中的数据 */ private val bytes = "风萧萧兮易水寒。".toByteArray() /** * 创建 MemoryFile 并返回 ParcelFileDescriptor */ private fun createMemoryFile(): ParcelFileDescriptor? { // 创建 MemoryFile 对象,1024 是最大占用内存的大小。 val file = MemoryFile("TestAshmemFile", 1024) // 获取文件描述符,因为方法被标注为 @hide,只能反射获取 val descriptor = invokeMethod("getFileDescriptor", file) as? FileDescriptor // 如果获取失败,返回 if (descriptor == null) { Log.i("ZHP", "获取匿名共享内存的 FileDescriptor 失败") return null } // 往共享内存中写入数据 file.writeBytes(bytes, 0, 0, bytes.size) // 因为要跨进程传递,需要序列化 FileDescriptor return ParcelFileDescriptor.dup(descriptor) } /** * 通过反射执行 obj.name() 方法 */ private fun invokeMethod(name: String, obj: Any): Any? { val method = obj.javaClass.getDeclaredMethod(name) return method.invoke(obj) }
MemoryFile 有两个构造方法,上面是一种,另一种是根据已有的 FileDescriptor 创建。 MemoryFile 创建时指定的大小并不是实际占用的物理内存大小,实际占用内存大小由写入的数据决定,但不能超过指定的大小。
3. 将文件描述符传递到其他进程
这里选择用 Binder 传递 ParcelFileDescriptor。 我们定义一个 Code,用于 C/S 两端通信确定事件:
/** * 两个进程在传递 FileDescriptor 时用到的 Code。 */ const val MY_TRANSACT_CODE = 920511
再在需要的地方 bindService:
// 创建服务进程 val intent = Intent(this, MyService::class.java) bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
bind 成功之后将 文件描述符 和 数据大小 序列化,然后通过 Binder 传递到 Service 进程:
private val serviceConnection = object: ServiceConnection { override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { if (binder == null) { return } // 创建 MemoryFile,并拿到 ParcelFileDescriptor val descriptor = createMemoryFile() ?: return // 传递 FileDescriptor 和 共享内存中数据的大小 val sendData = Parcel.obtain() sendData.writeParcelable(descriptor, 0) sendData.writeInt(bytes.size) // 保存对方进程的返回值 val reply = Parcel.obtain() // 开始跨进程传递 binder.transact(MY_TRANSACT_CODE, sendData, reply, 0) // 读取 Binder 执行的结果 val msg = reply.readString() Log.i("ZHP", "Binder 执行结果是:「$msg」") } override fun onServiceDisconnected(name: ComponentName?) {} }
两个进程的文件描述符指向同一个文件结构体,文件结构体指向了一片内存共享区域(ASMA),使得两个文件描述符对应到同一片ASMA中。
4. 在其他进程接收 FileDescriptor 并读取数据
先定义一个 MyService 用于开启子进程:
class MyService : Service() { private val binder by lazy { MyBinder() } override fun onBind(intent: Intent) = binder }
再实现具体的 MyBinder 类,主要包含3个步骤: 1. 从序列化数据中读取 FileDescriptor 和 共享内存中保存的数据大小; 2. 根据 FileDescriptor 创建 FileInputStream; 3. 读取共享内存中的数据。
/** * 这里不必使用 AIDL,继承 Binder 类 重写 onTransact 即可。 */ class MyBinder: Binder() { /** * 文件描述符 和 数据大小 通过 data 传入。 */ override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { val parent = super.onTransact(code, data, reply, flags) if (code != MY_TRANSACT_CODE && code != 931114) { return parent } // 读取 ParcelFileDescriptor 并转为 FileDescriptor val pfd = data.readParcelable<ParcelFileDescriptor>(javaClass.classLoader) if (pfd == null) { return parent } val descriptor = pfd.fileDescriptor // 读取共享内存中数据的大小 val size = data.readInt() // 根据 FileDescriptor 创建 InputStream val input = FileInputStream(descriptor) // 从 共享内存 中读取字节,并转为文字 val bytes = input.readBytes() val message = String(bytes, 0, size, Charsets.UTF_8) Log.i("ZHP", "读取到另外一个进程写入的字符串:「$message」") // 回复调用进程 reply?.writeString("Server 端收到 FileDescriptor, 并且从共享内存中读到了:「$message」") return true } }
这里拿到 FileDescriptor 后不仅可以读也能写入数据,还可以再创建一个 MemoryFile 对象。
以上就是详解Android Ashmem匿名共享内存的详细内容,更多关于Android Ashmem匿名共享内存的资料请关注脚本之家其它相关文章!