• 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 和 数据写入

    /**
    * 需要写入到共享内存中的数据
    */
    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    MemoryFile 有两个构造方法,上面是一种,另一种是根据已有的 FileDescriptor 创建。 MemoryFile 创建时指定的大小并不是实际占用的物理内存大小,实际占用内存大小由写入的数据决定,但不能超过指定的大小。

    3. 将文件描述符传递到其他进程

    这里选择用 Binder 传递 ParcelFileDescriptor。 我们定义一个 Code,用于 C/S 两端通信确定事件:

    /**
    * 两个进程在传递 FileDescriptor 时用到的 Code。
    */
    const val MY_TRANSACT_CODE = 920511
    
    • 1
    • 2
    • 3
    • 4

    再在需要的地方 bindService

    // 创建服务进程
    val intent = Intent(this, MyService::class.java)
    bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
    
    • 1
    • 2
    • 3

    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?) {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    两个进程的文件描述符指向同一个文件结构体,文件结构体指向了一片内存共享区域(ASMA),使得两个文件描述符对应到同一片ASMA中。

    4. 在其他进程接收 FileDescriptor 并读取数据

    先定义一个 MyService 用于开启子进程:

    class MyService : Service() {
        private val binder by lazy { MyBinder() }
        override fun onBind(intent: Intent) = binder
    }
    
    • 1
    • 2
    • 3
    • 4

    再实现具体的 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
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    这里拿到 FileDescriptor 后不仅可以读也能写入数据,还可以再创建一个 MemoryFile 对象。

    这就是Android匿名共享内存的使用啦~

  • 相关阅读:
    mac 查看本机 ip
    大数据毕业设计选题推荐-市天气预警实时监控平台-Hadoop-Spark-Hive
    讲座回顾丨基于 OpenYurt 和 EdgeX 的云边端协同新可能
    什么是正负样本
    操作系统实验 & bochs 环境配置
    每日一题 102二叉树的层序遍历
    Linux设备驱动
    原生js--购物车案例
    乐盟互动申请纳斯达克IPO上市,募资2000万美元
    本地服务器设置静态ip方法与原理
  • 原文地址:https://blog.csdn.net/lyabc123456/article/details/132892968