当前位置: 首页 > news >正文

Android匿名共享内存(Ashmem)

在Android中我们熟知的IPC方式有Socket文件ContentProviderBinder共享内存。其中共享内存的效率最高,可以做到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的文件描述符传递到进程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数组的索引,他们的关系如下图所示:

为了画图方便,这里将fd1fd2都写成了1,实际上每个进程被创建时,files的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。所以进程的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。

从图中可以看出fd1fd2 其实并没有直接的关系,那么进程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/ashmem"if (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;//取消内存映射的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系统函数mmapmunmap 函数,看一下man手册中的介绍

mmap, munmap - map or unmap files or devices into memory

  • mmap,映射文件或设备到内存中
  • munmap ,取消文件或设备到内存的映射

实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建 立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回 文件的。因此,采用共享内存的通信方式效率是非常高的。

看一下官方的注释,放了个连接,打开一看果然调用的mmap

//Os.java/*** See <a href="http://man7.org/linux/man-pages/man2/mmap.2.html">mmap(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(即你理解的内存块),物理页有页号,如果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 = 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/ashmem"if (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又想使用共享内存进行跨进程通信,又不想留下文件,同时也不想被其它的进程不小心打开了自己进程的文件,因此使用匿名共享内存的好处就是:

  1. 不用担心共享内存映射的文件被其它进程打开导致数据异常。
  2. 不会在硬盘中生成文件,使用匿名共享内存的方式主要是为了通信,而且通信是很频繁的,不希望因为通信而生成很多的文件,或者留下文件。

二、为什么叫匿名共享内存?明明通过iotc设置了名字的?

这个问题在我看来是我之前对匿名这个词有些误解,其实匿名并不是没有名字,而是无法通过这些明面上的信息找到实际的对象,就像马甲一样。匿名共享内存也正是如此,虽然我们设置了名字,但是另外的进程通过同样的名字创建匿名共享内存却并不指向同一个内存了(代码验证过),虽然名字相同,但是背后的人却已经换了。这同时也回答上个问题,为什么匿名共享内存不用担心被其它进程映射进行数据读写(除非经过自己的同意,也就是通过binder传递了文件描述符给另一个进程)。


Android系统共享内存

1.共享内存简介

共享内存是进程间通讯的一种方式,通过映射一块公共内存到各自的进程空间来达到共享内存的目的。
通常进程内存空间是4G,这个大小是由内存指针长度决定的,如果指针长度32位,那么地址最大编号为0xffffffff, 为4G。
上面的内存实际指的是进程的虚拟地址空间,还需要经过内存映射才能访问到真实的物理内存,这些工作对用户是透明的,不需要用户关心,操作系统都已经帮我们做好了。
通常虚拟内存地址和物理内存地址,但是存在一种对应关系。比如,进程操作的0x12345561这块内存地址,经过OS映射之后,可能实际的物理地址是0x87888312。
下图说明了虚拟内存与物理内存之间的关系。

两个不同的进程可以同时访问同一块内存吗?答案是肯定的。这就是内存共享,该机制由操作系统提供和实现。那么是如何做到的呢? Android平台上内存共享通常按如下做法实现:

  1. 进程A创建并打开一个文件(可以是设备文件/dev/ashmem),得到一个文件描述符fd.
  2. 通过mmap调用将fd映射成内存映射文件。在mmap调用中指定参数用于标识创建的是共享内存。
  3. 进程B打开同一个文件,也得到一个文件描述符,这样A和B就打开了同一个文件。
  4. 进程B也要用mmap调用指定参数表示想使用共享内存,并传递打开的fd。这样A和B就通过打开同一个文件并构造内存映射,实现进程间内存共享。

对于进程间需要传递大量数据的场景下,这种通信方式是十分高效的。

2. MemoryHeapBase与MemoryBase

Android在Native层通过MemoryHeapBase与MemoryBase两个类实现共享内存。

class AudioTrackJniStorage {public:sp<MemoryHeapBase>         mMemHeap;sp<MemoryBase>             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(fd<0, "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(fd<0, "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 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<IMemoryHeap>& heap, ssize_t offset, size_t size);virtual ~MemoryBase();virtual sp<IMemoryHeap> getMemory(ssize_t* offset, size_t* size) const;protected:size_t getSize() const { return mSize; } //返回大小ssize_t getOffset() const { return mOffset; } //返回偏移量// 返回MemoryHeapBase对象const sp<IMemoryHeap>& getHeap() const { return mHeap; }private:size_t          mSize;ssize_t         mOffset;sp<IMemoryHeap> mHeap;
};// ---------------------------------------------------------------------------
}; // namespace android

3. 流程总结

总结下使用MemoryHeapBase与MemoryBase实现共享内存的相关流程:

  1. 分配一块共享内存,这样两个进程可以共享这块内存
  2. 基于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.readParcelable<ParcelFileDescriptor>(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 对象。

相关文章:

Android匿名共享内存(Ashmem)

在Android中我们熟知的IPC方式有Socket、文件、ContentProvider、Binder、共享内存。其中共享内存的效率最高&#xff0c;可以做到0拷贝&#xff0c;在跨进程进行大数据传输&#xff0c;日志收集等场景下非常有用。共享内存是Linux自带的一种IPC机制&#xff0c;Android直接使用…...

MySOL之旅--------MySQL数据库基础( 3 )

本篇碎碎念:要相信啊,胜利就在前方,要是因为一点小事就停滞不前,可能你也不适合获取胜利,成功的路上会伴有泥石,但是走到最后,你会发现身上的泥泞皆是荣耀的勋章! 今日份励志文案: 凡是发生皆有利于我 目录 查询(select) 1.全列查询 2.指定列查询 3.查询字段为表达式 ​编…...

阿药陪你学Java(第零讲)

第零讲&#xff1a;基本数据类型 Java包括两种数据类型&#xff0c;分别是内置数据类型&#xff08;基本数据类型&#xff09;和引用数据类型。 内置数据类型 Java提供了8中内置类型&#xff0c;其中包括4种数字整型、2种数字浮点型、1中字符型、1中布尔型。下面进行详细介绍…...

华院计算参编《金融业人工智能平台技术要求》标准

随着人工智能技术的迅猛发展&#xff0c;金融机构正在从业务场景化向企业智能化演进&#xff0c;金融业对智能化的需求愈加迫切。为引导产业有序发展、规范行业自律、加快金融行业智能化转型&#xff0c;中国信通院依托中国人工智能产业发展联盟&#xff08;AIIA&#xff09;及…...

vue3-element-admin二次开发遇到的问题总结,持续更新中

vue3-element-admin 是基于 Vue3 Vite5 TypeScript5 Element-Plus Pinia 等主流技术栈构建的免费开源的后台管理前端模板&#xff08;配套后端源码&#xff09;。 一、定制Element-Plus主题 1.创建 variables.scss 变量文件 /*variables.scss*/ /*覆盖element-plus变量*/…...

SpringMVC数据接收(全面/详细注释)

SpringMVC涉及组件&#xff1a; DispatcherServlet : SpringMVC提供&#xff0c;我们需要使用web.xml配置使其生效&#xff0c;它是整个流程处理的核心&#xff0c;所有请求都经过它的处理和分发&#xff01;[ CEO ]HandlerMapping : SpringMVC提供&#xff0c;我们需要进行…...

golang 冒泡、选择、插入、快速排序法

个人学习笔记&#xff5e; 1. 冒泡排序 // Author sunwenbo // 2024/4/6 22:37 /* 1. 一共会经过arr.length -1 次的轮数比较&#xff0c;每一轮将会确认一个数的位置 2. 每一轮的比较次数逐渐的减少 [4,3,2,1] 3. 当发现前面的一个数比后面的一个数大的时候&#xff0c;就进行…...

vue3 +Taro 页面实现scroll-view 分页功能

需求 现在分页列表 后端只给你一个分页的数据列表 没有总页数 没有当前的分页 页数 只有这么一个list 、、、 如何去分页 我这使用的是scroll-view 组件 滑动到底部的事件 根据你当前设定的每页的数据数量和后端返回给你的数据列表数量 当某一次分页 两个数量不相等了以后 就…...

【http】常见http headers

相关文章&#xff1a;http 状态码 和http methods及restful api 常见http headers 1 常见的Request Headers Accept 浏览器可接收的数据格式 Accept-Encoding 浏览器可接收的压缩算法&#xff0c;gzip Accept-language 浏览器可接收的语言 Connection:keep-alive 一次TCP连接…...

Web App 入门指南:构建预测模型 App 的利器(shiny)

Web App 入门指南&#xff1a;构建预测模型 App 的利器 简介 近年来&#xff0c;随着机器学习和人工智能技术的快速发展&#xff0c;预测模型在各行各业得到了广泛应用。为了方便地部署和使用预测模型&#xff0c;将模型构建成 Web App 是一种非常好的选择。Web App 无需下载…...

6.7物联网RK3399项目开发实录-驱动开发之Camera摄像头的使用(wulianjishu666)

90款行业常用传感器单片机程序及资料【stm32,stc89c52,arduino适用】 链接&#xff1a;https://pan.baidu.com/s/1M3u8lcznKuXfN8NRoLYtTA?pwdc53f Camera 使用 简介 AIO-3399J 开发板分别带有两个 MIPI&#xff0c;MIPI 支持最高 4K 拍照&#xff0c;并支持 1080P 30fp…...

OSCP靶场-- Sybaris

OSCP靶场–Sybaris 考点(redis MODULE LOAD命令执行) 1.nmap扫描 ## ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.158.93 -sV -sC -Pn --min-rate 2500 -p- Starting Nmap 7.92 ( https://nmap.org ) at 2024-04-11 04:24 EDT Nmap scan report for 192.168.158.93…...

MyBatis 执行流程

加载配置文件:MvBatis 的执行流程从加载配置文件开始。通常&#xff0c;MyBatis 的配置文件是一个 XML 文件&#xff0c;其中包含了数据源配置、SQL 映射配置、连接池配置等信息。构建 SqlSessionFactory:在配置文件加载后&#xff0c;MyBatis 使用配置信息来构建 SqlSessionFa…...

android11 SystemUI入門之KeyguardPatternView解析

view层级树为&#xff1a; 被包含在 keyguard_host_view.xml中 。 <?xml version"1.0" encoding"utf-8"?> <!-- This is the host view that generally contains two sub views: the widget viewand the security view. --> <com.andro…...

doss攻击为什么是无解的?

这个让Google、亚马逊等实力巨头公司也无法避免的攻击。可以这么说&#xff0c;是目前最强大、最难防御的攻击之一&#xff0c;属于世界级难题&#xff0c;并且没有解决办法。 Doss攻击的原理不复杂&#xff0c;就是利用大量肉鸡仿照真实用户行为&#xff0c;使目标服务器资源…...

2. 如何让mybatis-plus的逻辑删除注解@TableLogic临时失效

文章目录 如何让mybatis-plus的逻辑删除注解TableLogic临时失效1. 场景复现1.1 controller代码1.2 service层代码1.3 entity代码 2. 问题分析3. 解决方案3.1 说明3.2 核心代码3.3 service方法对应修改为3.4 运行结果 如何让mybatis-plus的逻辑删除注解TableLogic临时失效 1. 场…...

基于单片机的智能窗帘系统设计

摘要: 介绍了一种智能窗帘系统,该系统由单片机STC89C52 为控制核心,光照控制电路采用光敏电阻和ADC0832 芯片,步进电机驱动电路采用ULN2003 芯片,以达到不同光照强度下窗帘的自动启闭。仿真和实物结果显示,该系统实用性强,应用范围广,具有很好的应用前景。 关键词: 单片…...

代码随想录 Day17 字符串 | LC344 反转字符串 LC541 反转字符串II 卡码网54替换数字

一、反转字符串 题目&#xff1a; 力扣344&#xff1a;反转字符串 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题…...

LeetCode 刷题汇总——题目序号顺序版

剑指 Offer——和为 S 的两个数字 剑指 Offer——数字在排序数组中出现的次数 剑指 Offer——和为 S 的连续正数序列 剑指 Offer——最小的 K 个数 剑指 Offer——连续子数组的最大和 剑指 Offer——数组中的逆序对 LeetCode 1——两数之和 LeetCode 2——两数相加 LeetCode 3…...

【Java】JDK1.8 HashMap源码,put源码详细讲解

&#x1f4dd;个人主页&#xff1a;哈__ 期待您的关注 在Java中&#xff0c;HashMap结构是被经常使用的&#xff0c;在面试当中也是经常会被问到的。这篇文章我给大家分享一下我对于HashMap结构源码的理解。 HashMap的存储与一般的数组不同&#xff0c;HashMap的每一个元素存…...

自定义注解进行数据转换

前言&#xff1a; Java注解是一种元数据机制&#xff0c;可用于方法&#xff0c;字段&#xff0c;类等程序上以提供关于这些元素的额外信息。 以下内容是我自己写的一个小测试的demo,参考该文章进行编写&#xff1a;https://blog.csdn.net/m0_71621983/article/details/1318164…...

React - 你知道在React组件的哪个阶段发送Ajax最合适吗

难度级别:中级及以上 提问概率:65% 如果求职者被问到了这个问题,那么只是单纯的回答在哪个阶段发送Ajax请求恐怕是不够全面的。最好是先详细描述React组件都有哪些生命周期,最后再回过头来点题作答,为什么应该在这个阶段发送Ajax请求。那…...

spa、vue、elementUi

spa (single page application). 动态重写当前页面而非从服务器重新加载整个新页面。使应用程序更像一个桌面应用程序。所有的html、javascript、css通过单个页面检索加载资源。前端页面使用ajax与后端通信。一个项目只有一个html页面。所有的页面跳转都通过路由导航。 vue可用…...

tcp接受命令执行并回显

为了实现循环执行命令并能够多次从TCP客户端接收命令&#xff0c;您需要对上面的代码进行一些修改。下面是一个修改后的示例&#xff0c;它将在接收到新的TCP连接后进入一个循环&#xff0c;不断地读取命令、执行命令&#xff0c;并将结果发送回客户端&#xff0c;直到客户端断…...

LLMs之ToolAlpaca:ToolAlpaca(通用工具学习框架/工具使用语料库)的简介、安装和使用方法、案例应用之详细攻略

LLMs之ToolAlpaca&#xff1a;ToolAlpaca(通用工具学习框架/工具使用语料库)的简介、安装和使用方法、案例应用之详细攻略 目录 ToolAlpaca的简介 0、《ToolAlpaca: Generalized Tool Learning for Language Models with 3000 Simulated Cases》翻译与解读 1、数据集列表 2…...

TCP/IP协议介绍

TCP/IP协议 先看&#xff1a;程序员必备基础知识-TCP/IP协议详解.(超详细&#xff09; 太厉害了&#xff0c;终于有人能把TCP/IP协议讲的明明白白了&#xff01; 面试官&#xff1a;如何理解TCP/IP协议? 一个很全的介绍博客&#xff1a;史上最全的TCP/IP协议原理 对TCP三次握…...

选择排序解读

在计算机科学中&#xff0c;排序算法是一种将数据元素按照某种顺序排列的算法。今天&#xff0c;我们要探讨的是选择排序&#xff08;Selection Sort&#xff09;&#xff0c;这是一种简单直观的排序方法&#xff0c;通过不断选择剩余元素中的最小&#xff08;或最大&#xff0…...

Vue项目自动注入less、sass、scss、stylus全局变量

一、Vue2项目 // vue.config.js const path require(path) module.exports {css: {loaderOptions: {// 给 sass-loader 传递选项sass: {// / 是 src/ 的别名// 所以这里假设有 src/assets/style/var.sass 这个文件// 注意&#xff1a;在 sass-loader v8 中&#xff0c;这个选…...

DXP学习002-PCB编辑器的环境参数及电路板参数相关设置

目录 一&#xff0c;dxp的pcb编辑器环境 1&#xff0c;创建新的PCB设计文档 2&#xff0c;PCB编辑器界面 1&#xff09;布线工具栏 2&#xff09;公用工具栏 3&#xff09;层标签栏 ​编辑 3&#xff0c;PCB设计面板 1&#xff09;打开pcb设计面板 4&#xff0c;PCB观…...

Flutter 使用flutter_swiper_null_safety 实现轮播图

目录 引入flutter_swiper_null_safety 在pubspec.yaml文件中dependencies下添加以下依赖 然后执行命令进行下载 实现轮播图 引入flutter_swiper_null_safety 在pubspec.yaml文件中dependencies下添加以下依赖 flutter_swiper_null_safety: ^1.0.2 然后执行命令进行下载 flu…...