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匿名共享内存的使用啦~

相关推荐
百锦再1 小时前
Android Studio开发 SharedPreferences 详解
android·ide·android studio
青春给了狗1 小时前
Android 14 修改侧滑手势动画效果
android
CYRUS STUDIO1 小时前
Android APP 热修复原理
android·app·frida·hotfix·热修复
火柴就是我2 小时前
首次使用Android Studio时,http proxy,gradle问题解决
android
limingade2 小时前
手机打电话时电脑坐席同时收听对方说话并插入IVR预录声音片段
android·智能手机·电脑·蓝牙电话·电脑打电话
浩浩测试一下3 小时前
计算机网络中的DHCP是什么呀? 详情解答
android·网络·计算机网络·安全·web安全·网络安全·安全架构
青春给了狗4 小时前
Android 14 系统统一修改app启动时图标大小和圆角
android
pengyu5 小时前
【Flutter 状态管理 - 柒】 | InheritedWidget:藏在组件树里的"魔法"✨
android·flutter·dart
居然是阿宋6 小时前
Kotlin高阶函数 vs Lambda表达式:关键区别与协作关系
android·开发语言·kotlin
凉、介7 小时前
PCI 总线学习笔记(五)
android·linux·笔记·学习·pcie·pci