Android匿名共享内存(Ashmem)
在Android中我们熟知的IPC方式有Socket、文件、ContentProvider、Binder、共享内存。其中共享内存的效率最高,可以做到0拷贝,在跨进程进行大数据传输,日志收集等场景下非常有用。共享内存是Linux自带的一种IPC机制,Android直接使用使用了该模型,不过做出了自己的改进,进而形成了Android的匿名共享内存。
本文将会通过android提供的MemoryFile源码来分析如何使用匿名共享内存,并使用native层代码实现一个简易版的MemoryFile。
MemoryFile简单使用
//MainActivity.kt 进程1 class MainActivity : AppCompatActivity() { var mBinder: Binder? = null val memoryFile:MemoryFile? = null private var mConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { mBinder = service as Binder } override fun onServiceDisconnected(className: ComponentName) { mBinder = null } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val intent = Intent(this, TestShareMemoryService::class.java) startService(intent) bindService(intent, mConnection, Context.BIND_AUTO_CREATE) } //1.创建共享内存,并通过binder传递文件描述符 fun createMemoryFile(view: View) { //参数1文件名可为null,参数2文件大小 memoryFile = MemoryFile("test", 1024) memoryFile?.apply { mBinder?.apply { val data = Parcel.obtain() val reply = Parcel.obtain() val getFileDescriptorMethod: Method = memoryFile.getClass().getDeclaredMethod("getFileDescriptor") val fileDescriptor = getFileDescriptorMethod.invoke(memoryFile) // 序列化,才可传送 val pfd = ParcelFileDescriptor.dup(fileDescriptor) data.writeFileDescriptor(fileDescriptor) transact(TestShareMemoryService.TRANS_CODE_SET_FD, data, reply, 0) } } } //2.写入数据 fun write(data:ByteArray) { memoryFile.write(data, 0, 0, data.size); } }
//MainActivity2.kt 进程2 class MainActivity2 : AppCompatActivity() { var mBinder: IBinder? = null private var mConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { mBinder = service } override fun onServiceDisconnected(className: ComponentName) { mBinder = null } } fun read(view: View) { val data = Parcel.obtain() val reply = Parcel.obtain() mBinder?.apply { //从服务端获取MainActivity传递的文件描述符 transact(TestShareMemoryService.TRANS_CODE_GET_FD, data, reply, 0) var fi: FileInputStream? = null var fileDescriptor: FileDescriptor? = null try { val pfd = reply.readFileDescriptor() if (pfd == null) { return } fileDescriptor = pfd.fileDescriptor fi = FileInputStream(fileDescriptor) //读取数据 fi.read(buffer) } } catch (e: RemoteException) { e.printStackTrace() } catch (e: IOException) { e.printStackTrace() } finally { if (fileDescriptor != null) { try { fi.close() } catch (e: IOException) { e.printStackTrace() } } } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main2) val intent = Intent(this, TestShareMemoryService::class.java) startService(intent) bindService(intent, mConnection, Context.BIND_AUTO_CREATE) } }
//TestShareMemoryService.kt class TestShareMemoryService : Service() { lateinit var fd: ParcelFileDescriptor companion object { const val TRANS_CODE_GET_FD = 0x0000 const val TRANS_CODE_SET_FD = 0x0001 } override fun onBind(intent: Intent?): IBinder { return TestBinder() } inner class TestBinder : Binder() { override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { when (code) { TRANS_CODE_SET_FD -> { //保存创建共享内存进程传递过来的文件描述符 fd = data.readFileDescriptor() } TRANS_CODE_GET_FD -> { //将文件描述符传递给请求的进程 reply?.writeFileDescriptor(fd.fileDescriptor) } } return true } } }
梳理一下流程
- 1、进程1创建MemoryFile并写入数据
- 2、通过Binder将MemoryFile的文件描述符传递到进程2
- 3、进程2通过获取到的文件描述符进行数据的读写
这里流程中的第二步有一个问题,从进程1将文件描述符传递到进程2,那么这两个进程的文件描述符是同一个吗?
答案是这两个文件描述符并不是同一个,只不过他们都指向了内核中的同一个文件。
文件描述符
linux系统中的文件描述符是什么?在回答这个问题前先来看一下linux系统中进程是什么?
在linux系统中进程实际上就是一个结构体,而且线程和进程使用的是同一个结构体,其部分源码如下:
struct task_struct { // 进程状态 long state; // 虚拟内存结构体 struct mm_struct *mm; // 进程号 pid_t pid; // 指向父进程的指针 struct task_struct __rcu *parent; // 子进程列表 struct list_head children; // 存放文件系统信息的指针 struct fs_struct *fs; // 一个数组,包含该进程打开的文件指针 struct files_struct *files; };
可以看到在结构体中有一个files字段,它记录着该进程打开的文件指针,而我们所说的文件描述符实际上就是这个files数组的索引,他们的关系如下图所示:
为了画图方便,这里将fd1和fd2都写成了1,实际上每个进程被创建时,files的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。所以进程的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。
从图中可以看出fd1 和fd2 其实并没有直接的关系,那么进程2是如何通过进程1的fd1 来生成一个同fd1 指向同一个 文件呢?
回想一下我们是怎么把fd1 转成fd2 的,是通过Binder#transact 方法实现的,因此我们来看一下Binder 的源码是如何做的
//Binder.c static void binder_transaction(struct binder_proc *proc, struct binder_thread *thread, struct binder_transaction_data *tr, int reply) { switch(fp->type) { case BINDER_TYPE_FD: { int target_fd; struct file *file; // 通过进程1的fp->handle获取到真正的文件,在内核中是唯一的fd指向它 file = fget(fp->handle); // 获取目标进程中未使用的文件描述符fd target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC); // 将目标进程的文件描述符fd和该file进行配对,这样目标进程就能通过target_fd找到file task_fd_install(target_proc, target_fd, file); } break; } }
看了源码我们发现原理非常简单,其实就是通过内核中的Binder帮我们进行转换的,因为内核是有所有用户进程信息,所以它可以轻松的做到这一点。
还有一点需要说明的是,在上图中的file1,file2,file3并不一定是存在磁盘上的物理文件,也有可能是抽象的文件(虚拟文件),而本篇文章说的匿名共享内存 实际上就是映射到一个虚拟的文件,至于这块的内容可以看一下Linux的tmpfs文件系统 。
MemoryFile源码解析
共享内存的基础知识上面做了简单的介绍,现在来看看Android是如何做的。MemoryFile 是Android提供的java层匿名共享内存工具,通过它的源码来跟踪整个流程。
相关文件列表:
frameworks/base/core/java/android/os/ - MemoryFile.java - SharedMemory.java frameworks/base/core/jni/android_os_SharedMemory.cpp system/core/libcutils/ashmem-dev.cpp
//MemoryFile.java public MemoryFile(String name, int length) throws IOException { //通过SharedMemory创建匿名共享内存 mSharedMemory = SharedMemory.create(name, length); //映射 mMapping = mSharedMemory.mapReadWrite(); }
//SharedMemory public static @NonNull SharedMemory create(@Nullable String name, int size) throws ErrnoException { //实际上调用了native层去创建匿名共享内存,并返回文件描述符 return new SharedMemory(nCreate(name, size)); } private static native FileDescriptor nCreate(String name, int size) throws ErrnoException;
//android_os_SharedMemory.cpp jobject SharedMemory_nCreate(JNIEnv* env, jobject, jstring jname, jint size) { const char* name = jname ? env->GetStringUTFChars(jname, nullptr) : nullptr; //调用ashmem_create_region来创建匿名共享内存 int fd = ashmem_create_region(name, size); //... jobject jifd = jniCreateFileDescriptor(env, fd); if (jifd == nullptr) { close(fd); } return jifd; }
// ashmem-dev.cpp int ashmem_create_region(const char *name, size_t size) { int ret, save_errno; //打开匿名共享内存对应的虚拟文件,最终调用到 __ashmem_open_locked() int fd = __ashmem_open(); if (fd
以上是获取匿名共享内存的文件描述符流程,总结一下核心的部分,只例举Android Q之前:
- 1、open("/dev/ashmem", O_RDWR | O_CLOEXEC),打开虚拟文件
- 2、ioctl(fd, ASHMEM_SET_NAME, buf),设置名字
- 3、ioctl(fd, ASHMEM_SET_SIZE, size),设置大小
接下来来看一下如何通过文件描述符 映射到共享内存中
如上面分析的代码,在MemoryFile的构造函数中先调用了SharedMemory#create(name, size)方法创建了匿名文件,之后调用SharedMemory.mapReadOnly() 来将匿名文件映射到共享内存中,最终调用到了如下方法中:
//SharedMemory.java public @NonNull ByteBuffer map(int prot, int offset, int length) throws ErrnoException { //通过mFileDescriptor文件描述符进行内存映射,并返回内存地址 long address = Os.mmap(0, length, prot, OsConstants.MAP_SHARED, mFileDescriptor, offset); boolean readOnly = (prot & OsConstants.PROT_WRITE) == 0; //取消内存映射的Runnable,run方法中会调用Os.munmap(mAddress, mSize); Runnable unmapper = new Unmapper(address, length, mMemoryRegistration.acquire()); //使用DirectByteBuffer直接对内存进行读写操作 return new DirectByteBuffer(length, address, mFileDescriptor, unmapper, readOnly); }
如果对linux系统熟悉的话看到Os.mmap() 和Os.munmap() 方法应该能知道内存映射实际上就是调用的linux系统函数mmap 和munmap 函数,看一下man手册中的介绍
mmap, munmap - map or unmap files or devices into memory
- mmap,映射文件或设备到内存中
- munmap ,取消文件或设备到内存的映射
实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建 立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回 文件的。因此,采用共享内存的通信方式效率是非常高的。
看一下官方的注释,放了个连接,打开一看果然调用的mmap
//Os.java /** * See mmap(2). */ public static long mmap(long address, long byteCount, int prot, int flags, FileDescriptor fd, long offset) throws ErrnoException { return Libcore.os.mmap(address, byteCount, prot, flags, fd, offset); }
至此,MemoryFile 源码的核心可以说是分析完了。
最后,稍微提一下linux内存映射的原理:
linux下,内存采用分页存储,一个物理页的大小是4k(即你理解的内存块),物理页有页号,如果a,b两个进程共享了8k的内存,比如代码区相同,则在双方进程的页表中(线性地址到物理地址的转换表,linux下逻辑地址和线性地址相同)会将各自的线性地址映射到那两个相同的物理页面上去。实际上内存就只有一份数据。
native实现一个简易版MemoryFile
现在来自定义一个MemoryFile,用到核心方法:
open(ASHMEM_NAME_DEF, O_RDWR); mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ioctl(fd, ASHMEM_SET_NAME, name); ioctl(fd, ASHMEM_SET_SIZE, size); munmap((void *) addr, size);
第一步我们先把api接口定义出来,代码如下
class MyShareMemory(fd: Int) { private val mFd: Int = fd private val mSize: Int init { mSize = nGetSize(mFd) require(mSize > 0) { "FileDescriptor is not a valid ashmem fd" } } //获取可以使用Binder传输文件描述符的对象,用于跨进程传输文件描述符 fun getFileDescriptor(): FileDescriptor { return ParcelFileDescriptor.fromFd(mFd).fileDescriptor; } companion object { init { System.loadLibrary("mysharememory-lib") } fun create(name: String, size: Int): MyShareMemory { require(size > 0) { "Size must be greater than zero" } return MyShareMemory(nCreate(name, size)) } //创建需要映射的匿名文件 @JvmStatic private external fun nCreate(name: String, size: Int): Int //获取大小 @JvmStatic private external fun nGetSize(fd: Int): Int //关闭文件并解除映射 @JvmStatic private external fun nClose(fd: Int) //写数据,这里的offset只设置了destOffset,没有写srcOffset,可以完善,nRead同理 @JvmStatic private external fun nWrite(fd: Int, size: Int, offset: Int, data: ByteArray): Int //读数据 @JvmStatic private external fun nRead(fd: Int, size: Int, offset: Int, data: ByteArray): Int } }
接下来来实现这5个jni方法
extern "C" JNIEXPORT jint JNICALL Java_com_baidu_ryujin_ktc_MyShareMemory_nCreate(JNIEnv *env, jclass clazz, jstring name, jint size) { char *addr; int64_t ufd = 0; const char *_name = env->GetStringUTFChars(name, 0); //打开匿名文件并进行映射,addr为映射内存的地址,ufd为文件描述符 int ret = create_shared_memory(_name, size, addr, ufd); env->ReleaseStringUTFChars(name, _name); return ufd; }extern "C" JNIEXPORT jint JNICALL Java_com_baidu_ryujin_ktc_MyShareMemory_nGetSize(JNIEnv *env, jclass clazz, jint fd) { return get_shared_memory_size(fd); }extern "C" JNIEXPORT void JNICALL Java_com_baidu_ryujin_ktc_MyShareMemory_nClose(JNIEnv *env, jclass clazz, jint fd) { char *addr; //这里调用open去映射内存是为了获取addr,因为取消映射需要用到,这里是为了方便这么做,实际使用中可以保存起来 open_shared_memory(addr, fd); close_shared_memory(fd, addr); }extern "C" JNIEXPORT jint JNICALL Java_com_baidu_ryujin_ktc_MyShareMemory_nWrite(JNIEnv *env, jclass clazz, jint fd, jint size, jint offset, jbyteArray data_) { char *addr; int space = get_shared_memory_size(fd) - offset; if (size - space > 0) { return -1; } //同close一样,这里也是为了获取addr open_shared_memory(addr, fd); jbyte *data = env->GetByteArrayElements(data_, 0); //获取到共享内存地址后直接往里面写数据就行了 memcpy(addr + offset, data, size); env->ReleaseByteArrayElements(data_, data, 0); return 0; }extern "C" JNIEXPORT jint JNICALL Java_com_baidu_ryujin_ktc_MyShareMemory_nRead(JNIEnv *env, jclass clazz, jint fd, jint size, jint offset, jbyteArray data_) { //... return 0; }
核心实现代码
int create_shared_memory(const char *name, int64_t size, char *&addr, int64_t &fd) { fd = open(ASHMEM_NAME_DEF, O_RDWR);//#define ASHMEM_NAME_DEF "dev/ashmem" if (fd 0) {//改fd已经映射,直接获取地址 addr = (char *) mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); return 1; } else {//未映射 int ret = ioctl(fd, ASHMEM_SET_NAME, name);//设置名称 if (ret 0) { addr = (char *) mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); } else { return -1; } return 0; } int close_shared_memory(int64_t fd, char *&addr) { int size = get_shared_memory_size(fd); if (size
现在就可以像MemoryFile一样使用自定义的MemoryFile进行跨进程数据传输了,具体的可以github上的demo: https://github.com/GhRyuJin/CustomAnroidShareMemory。
最后讨论一下两个问题,以下仅为个人思考,欢迎补充和指正:
一、Android为什么设计一个匿名共享内存,共享内存不能满足需求吗?
首先我们来思考一下共享内存和Android匿名共享内存最大的区别,那就是共享内存往往映射的是一个硬盘中真实存在的文件,而Android的匿名共享内存映射的一个虚拟文件。这说明Android又想使用共享内存进行跨进程通信,又不想留下文件,同时也不想被其它的进程不小心打开了自己进程的文件,因此使用匿名共享内存的好处就是:
- 不用担心共享内存映射的文件被其它进程打开导致数据异常。
- 不会在硬盘中生成文件,使用匿名共享内存的方式主要是为了通信,而且通信是很频繁的,不希望因为通信而生成很多的文件,或者留下文件。
二、为什么叫匿名共享内存?明明通过iotc设置了名字的?
这个问题在我看来是我之前对匿名这个词有些误解,其实匿名并不是没有名字,而是无法通过这些明面上的信息找到实际的对象,就像马甲一样。匿名共享内存也正是如此,虽然我们设置了名字,但是另外的进程通过同样的名字创建匿名共享内存却并不指向同一个内存了(代码验证过),虽然名字相同,但是背后的人却已经换了。这同时也回答上个问题,为什么匿名共享内存不用担心被其它进程映射进行数据读写(除非经过自己的同意,也就是通过binder传递了文件描述符给另一个进程)。
Android系统共享内存
1.共享内存简介
共享内存是进程间通讯的一种方式,通过映射一块公共内存到各自的进程空间来达到共享内存的目的。
通常进程内存空间是4G,这个大小是由内存指针长度决定的,如果指针长度32位,那么地址最大编号为0xffffffff, 为4G。
上面的内存实际指的是进程的虚拟地址空间,还需要经过内存映射才能访问到真实的物理内存,这些工作对用户是透明的,不需要用户关心,操作系统都已经帮我们做好了。
通常虚拟内存地址和物理内存地址,但是存在一种对应关系。比如,进程操作的0x12345561这块内存地址,经过OS映射之后,可能实际的物理地址是0x87888312。
下图说明了虚拟内存与物理内存之间的关系。
两个不同的进程可以同时访问同一块内存吗?答案是肯定的。这就是内存共享,该机制由操作系统提供和实现。那么是如何做到的呢? Android平台上内存共享通常按如下做法实现:
- 进程A创建并打开一个文件(可以是设备文件/dev/ashmem),得到一个文件描述符fd.
- 通过mmap调用将fd映射成内存映射文件。在mmap调用中指定参数用于标识创建的是共享内存。
- 进程B打开同一个文件,也得到一个文件描述符,这样A和B就打开了同一个文件。
- 进程B也要用mmap调用指定参数表示想使用共享内存,并传递打开的fd。这样A和B就通过打开同一个文件并构造内存映射,实现进程间内存共享。
对于进程间需要传递大量数据的场景下,这种通信方式是十分高效的。
2. MemoryHeapBase与MemoryBase
Android在Native层通过MemoryHeapBase与MemoryBase两个类实现共享内存。
class AudioTrackJniStorage { public: sp mMemHeap; sp mMemBase; ....... ~AudioTrackJniStorage() { mMemBase.clear(); mMemHeap.clear(); } bool allocSharedMem(int sizeInBytes) { //先new一个MemoryHeapBase,再以它为参数new一个MemoryBase //(1) MemoryHeapBase mMemHeap = new MemoryHeapBase(sizeInBytes, 0, "AudioTrack Heap Base"); if (mMemHeap->getHeapID()
MemoryHeapBase与MemoryBase 类关系继承图如下
MemoryHeapBase是一个Binder类,承担BnMemoryHeapBase的角色, 实例由服务端创建,BpMemoryHeapBase 由客户端使用。
MemoryHeapBase有多个构造函数,创建共享内存方式不同, 使用时按需选择
[–>MemoryHeapBase.cpp ]
MemoryHeapBase::MemoryHeapBase() : mFD(-1), mSize(0), mBase(MAP_FAILED), mDevice(NULL), mNeedUnmap(false), mOffset(0) { } //通过ashmem设备创建共享内存,上size表示共享内存大小,flag为0, name为"AudioTrack Heap Base" MemoryHeapBase::MemoryHeapBase(size_t size, uint32_t flags, char const * name) : mFD(-1), mSize(0), mBase(MAP_FAILED), mFlags(flags), mDevice(0), mNeedUnmap(false), mOffset(0) { const size_t pagesize = getpagesize(); //获取系统内存页大小,一般为4kb size = ((size + pagesize-1) & ~(pagesize-1)); //创建共享内存, ashmem_create_region函数由libcutils提供, 真实设备上将打开/dev/ashmem设备得到一个fd int fd = ashmem_create_region(name == NULL ? "MemoryHeapBase" : name, size); ALOGE_IF(fd= 0) { //将通过mmap方式得到内存地址 if (mapfd(fd, size) == NO_ERROR) { if (flags & READ_ONLY) { //设置只读方式 ashmem_set_prot_region(fd, PROT_READ); } } } } /* 从指定设备创建共享内存 * maps memory from the given device */ MemoryHeapBase::MemoryHeapBase(const char* device, size_t size, uint32_t flags) : mFD(-1), mSize(0), mBase(MAP_FAILED), mFlags(flags), mDevice(0), mNeedUnmap(false), mOffset(0) { int open_flags = O_RDWR; if (flags & NO_CACHING) open_flags |= O_SYNC; int fd = open(device, open_flags); ALOGE_IF(fd= 0) { const size_t pagesize = getpagesize(); size = ((size + pagesize-1) & ~(pagesize-1)); if (mapfd(fd, size) == NO_ERROR) { mDevice = device; } } } /* 映射指定文件描述符指向的内存, 使用dup()方式copy * maps the memory referenced by fd. but DOESN'T take ownership * of the filedescriptor (it makes a copy with dup() */ MemoryHeapBase::MemoryHeapBase(int fd, size_t size, uint32_t flags, uint32_t offset) : mFD(-1), mSize(0), mBase(MAP_FAILED), mFlags(flags), mDevice(0), mNeedUnmap(false), mOffset(0) { const size_t pagesize = getpagesize(); size = ((size + pagesize-1) & ~(pagesize-1)); mapfd(fcntl(fd, F_DUPFD_CLOEXEC, 0), size, offset); } ...... }
MemoryHeapBase 类成员变量说明:
int mFD; //ashmem_crate_region返回的文件描述符 size_t mSize; //所要分配内存大小 void* mBase;//变量指向共享内存起始地址 uint32_t mFlags; const char* mDevice; //指定设备 bool mNeedUnmap; uint32_t mOffset; //内存偏移量
MemoryHeapBase 使用了引用计数、延迟分配物理内存(使用时才分配)等手段优化了传统内存共享方式。
MemoryBase也是一个Binder类,其声明在MemoryBase.h中,内容很简单,一起看下:
class MemoryBase : public BnMemory { public: //构造函数 MemoryBase(const sp& heap, ssize_t offset, size_t size); virtual ~MemoryBase(); virtual sp getMemory(ssize_t* offset, size_t* size) const; protected: size_t getSize() const { return mSize; } //返回大小 ssize_t getOffset() const { return mOffset; } //返回偏移量 // 返回MemoryHeapBase对象 const sp& getHeap() const { return mHeap; } private: size_t mSize; ssize_t mOffset; sp mHeap; }; // --------------------------------------------------------------------------- }; // namespace android
3. 流程总结
总结下使用MemoryHeapBase与MemoryBase实现共享内存的相关流程:
- 分配一块共享内存,这样两个进程可以共享这块内存
- 基于Binder通信,这样这两个类可以跨进程交互。
另外说明下: 这两个类没有提供同步对象保护这块共享内存, 在使用流程中必然需要提供一个跨进程的同步对象保护它。
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) }
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(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 对象。