Android 匿名共享内存的使用

注:本文内容转载自如下文章:Android 匿名共享内存的使用

Android View 的绘制是如何把数据传递给 SurfaceFlinger 的呢? 跨进程通信时,数据量大于1MB要怎么传递呢?用 匿名共享内存(Ashmem) 是个不错的选择,它不仅可以减少内存复制的次数,还没有内存大小的限制。这篇文章介绍在 Java 层如何使用匿名共享内存在进程间传递数据。

1. 简述

Android匿名共享内存(Ashmem) 基于 Linux 的共享内存 ,都是在临时文件系统(tmpfs)上创建虚拟文件,再映射到不同的进程。它可以让多个进程操作同一块内存区域,并且除了物理内存限制,没有其他大小限制。相对于 Linux 的共享内存,Ashmem 对内存的管理更加精细化,并且添加了互斥锁Java 层在使用时需要用到 MemoryFile ,它封装了 native 代码。

Java 层使用匿名共享内存的4个点:

  1. 通过 MemoryFile 开辟内存空间,获得 FileDescriptor
  2. FileDescriptor 传递给其他进程;
  3. 往共享内存写入数据;
  4. 从共享内存读取数据。

下面用一个例子介绍匿名共享内存的使用,假设需要开辟一段共享内存,写入一些数据,再在另外一个进程读取这段数据。

2. 创建 MemoryFile 和 数据写入

kotlin 复制代码
/**
* 需要写入到共享内存中的数据
*/
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 两端通信确定事件:

kotlin 复制代码
/**
* 两个进程在传递 FileDescriptor 时用到的 Code。
*/
const val MY_TRANSACT_CODE = 920511

再在需要的地方 bindService

kotlin 复制代码
// 创建服务进程
val intent = Intent(this, MyService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)

bind 成功之后将 文件描述符数据大小 序列化,然后通过 Binder 传递到 Service 进程:

kotlin 复制代码
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 用于开启子进程:

kotlin 复制代码
class MyService : Service() {
    private val binder by lazy { MyBinder() }
    override fun onBind(intent: Intent) = binder
}

再实现具体的 MyBinder 类,主要包含3个步骤:

  1. 从序列化数据中读取 FileDescriptor 和 共享内存中保存的数据大小;
  2. 根据 FileDescriptor 创建 FileInputStream
  3. 读取共享内存中的数据。
kotlin 复制代码
/**
* 这里不必使用 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匿名共享内存的使用啦~

相关推荐
sun00770020 小时前
android ndk编译valgrind
android
AI视觉网奇21 小时前
android studio 断点无效
android·ide·android studio
jiaxi的天空1 天前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet1 天前
android组包时会把从maven私服获取的包下载到本地吗
android
catchadmin1 天前
PHP serialize 序列化完全指南
android·开发语言·php
tangweiguo030519871 天前
Kable使用指南:Android BLE开发的现代化解决方案
android·kotlin
00后程序员张1 天前
iOS App 混淆与资源保护:iOS配置文件加密、ipa文件安全、代码与多媒体资源防护全流程指南
android·安全·ios·小程序·uni-app·cocoa·iphone
柳岸风1 天前
Android Studio Meerkat | 2024.3.1 Gradle Tasks不展示
android·ide·android studio
编程乐学1 天前
安卓原创--基于 Android 开发的菜单管理系统
android
whatever who cares1 天前
android中ViewModel 和 onSaveInstanceState 的最佳使用方法
android