asp做的网站,wordpress实现浮动联系,竞价sem培训,wordpress 用户 仪表盘在Android中我们熟知的IPC方式有Socket、文件、ContentProvider、Binder、共享内存。其中共享内存的效率最高#xff0c;可以做到0拷贝#xff0c;在跨进程进行大数据传输#xff0c;日志收集等场景下非常有用。共享内存是Linux自带的一种IPC机制#xff0c;Android直接使用…在Android中我们熟知的IPC方式有Socket、文件、ContentProvider、Binder、共享内存。其中共享内存的效率最高可以做到0拷贝在跨进程进行大数据传输日志收集等场景下非常有用。共享内存是Linux自带的一种IPC机制Android直接使用使用了该模型不过做出了自己的改进进而形成了Android的匿名共享内存。
本文将会通过android提供的MemoryFile源码来分析如何使用匿名共享内存并使用native层代码实现一个简易版的MemoryFile。
MemoryFile简单使用
//MainActivity.kt 进程1
class MainActivity : AppCompatActivity() {var mBinder: Binder? nullval memoryFile:MemoryFile? nullprivate 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? nullprivate 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? nullvar fileDescriptor: FileDescriptor? nulltry {val pfd reply.readFileDescriptor()if (pfd null) {return}fileDescriptor pfd.fileDescriptorfi 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: ParcelFileDescriptorcompanion object {const val TRANS_CODE_GET_FD 0x0000const 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的文件描述符传递到进程23、进程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.cstatic 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);// 获取目标进程中未使用的文件描述符fdtarget_fd task_get_unused_fd_flags(target_proc, O_CLOEXEC);// 将目标进程的文件描述符fd和该file进行配对这样目标进程就能通过target_fd找到filetask_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 0) {return fd;}if (name) {char buf[ASHMEM_NAME_LEN] {0};strlcpy(buf, name, sizeof(buf));//通过ioctl设置名字TEMP_FAILURE_RETRY宏定义会让返回的结果为false时一直重试//ioctl是系统调用用户进程和内存进行交互内部调用copy_from_user获取到用户进程传递的数据ret TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_NAME, buf));if (ret 0) {goto error;}}//设置匿名共享文件大小ret TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_SIZE, size));if (ret 0) {goto error;}return fd;
error:save_errno errno;close(fd);errno save_errno;return ret;
}static std::string get_ashmem_device_path() {static const std::string boot_id_path /proc/sys/kernel/random/boot_id;std::string boot_id;if (!android::base::ReadFileToString(boot_id_path, boot_id)) {ALOGE(Failed to read %s: %s.\n, boot_id_path.c_str(), strerror(errno));return ;};boot_id android::base::Trim(boot_id);return /dev/ashmem boot_id;
}static int __ashmem_open_locked()
{//获取匿名共享内存路径Android Q之后使用这个方式获取static const std::string ashmem_device_path get_ashmem_device_path();if (ashmem_device_path.empty()) {return -1;}//打开匿名共享内存使用的虚拟文件int fd TEMP_FAILURE_RETRY(open(ashmem_device_path.c_str(), O_RDWR | O_CLOEXEC));// Android Q之前的设备这里fd 0使用原来的路径/dev/ashmemif (fd 0) {int saved_errno errno;//打开匿名共享内存使用的虚拟文件fd TEMP_FAILURE_RETRY(open(/dev/ashmem, O_RDWR | O_CLOEXEC));if (fd 0) {return fd;}}//...return 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.javapublic 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;//取消内存映射的Runnablerun方法中会调用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 a hrefhttp://man7.org/linux/man-pages/man2/mmap.2.htmlmmap(2)/a.*/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(即你理解的内存块)物理页有页号如果ab两个进程共享了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 fdprivate val mSize: Intinit {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))}//创建需要映射的匿名文件JvmStaticprivate external fun nCreate(name: String, size: Int): Int//获取大小JvmStaticprivate external fun nGetSize(fd: Int): Int//关闭文件并解除映射JvmStaticprivate external fun nClose(fd: Int)//写数据这里的offset只设置了destOffset没有写srcOffset可以完善nRead同理JvmStaticprivate external fun nWrite(fd: Int, size: Int, offset: Int, data: ByteArray): Int//读数据JvmStaticprivate 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一样这里也是为了获取addropen_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/ashmemif (fd 0) {return -1;}int len get_shared_memory_size(fd);if (len 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) {close(fd);return -1;}ret ioctl(fd, ASHMEM_SET_SIZE, size);//设置大小if (ret 0) {close(fd);return -1;}//内存映射addr (char *) mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);}return 0;
}int open_shared_memory(char *addr, int64_t fd) {int size get_shared_memory_size(fd);if (size 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 0) {return -1;}//取消映射int ret munmap((void *) addr, size);if (ret -1) {return -1;}ret close(fd);if (ret -1) {return -1;}return 0;
}int get_shared_memory_size(int64_t fd) {return ioctl(fd, ASHMEM_GET_SIZE, NULL);
}现在就可以像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:spMemoryHeapBase mMemHeap;spMemoryBase mMemBase;.......
~AudioTrackJniStorage() {mMemBase.clear();mMemHeap.clear();}bool allocSharedMem(int sizeInBytes) {//先new一个MemoryHeapBase再以它为参数new一个MemoryBase//(1) MemoryHeapBasemMemHeap new MemoryHeapBase(sizeInBytes, 0, AudioTrack Heap Base);if (mMemHeap-getHeapID() 0) {return false;}//(2) MemoryBasemMemBase new MemoryBase(mMemHeap, 0, sizeInBytes);return true;}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(); //获取系统内存页大小一般为4kbsize ((size pagesize-1) ~(pagesize-1));//创建共享内存 ashmem_create_region函数由libcutils提供 真实设备上将打开/dev/ashmem设备得到一个fdint fd ashmem_create_region(name NULL ? MemoryHeapBase : name, size);ALOGE_IF(fd0, error creating ashmem region: %s, strerror(errno));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(fd0, error opening %s: %s, device, strerror(errno));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 DOESNT 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 spIMemoryHeap heap, ssize_t offset, size_t size);virtual ~MemoryBase();virtual spIMemoryHeap getMemory(ssize_t* offset, size_t* size) const;protected:size_t getSize() const { return mSize; } //返回大小ssize_t getOffset() const { return mOffset; } //返回偏移量// 返回MemoryHeapBase对象const spIMemoryHeap getHeap() const { return mHeap; }private:size_t mSize;ssize_t mOffset;spIMemoryHeap 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)// 因为要跨进程传递需要序列化 FileDescriptorreturn 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并拿到 ParcelFileDescriptorval 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 并转为 FileDescriptorval pfd data.readParcelableParcelFileDescriptor(javaClass.classLoader)if (pfd null) {return parent}val descriptor pfd.fileDescriptor// 读取共享内存中数据的大小val size data.readInt()// 根据 FileDescriptor 创建 InputStreamval 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 对象。