Android系统匿名共享内存Ashmem

首先来看一下什么是匿名共享内存(Ashmem)。Ashmem是一种共享内存的机制,它利用了Linux的mmap系统调用,将不同进程中的同一段物理内存映射到进程各自的虚拟地址空间,从而实现高效的进程间共享。它以驱动程序的形式实现在内核空间。它有两个特点,一是能够辅助内存管理系统来有效地管理不再使用的内存块,二是它通过Binder进程间通信机制来实现进程间的内存共享。Ashmem的两个特点就是共享和高效。共享是指可以在不同进程间共享信息,高效则是因为不同进程都是直接进行的内存操作,相对于其他的进程间通信方式来讲,这种方式会更快一些。
android系统的匿名共享内存系统的主体是以驱动程序的形式实现在内核空间的,同时,在系统运行时库和应用程序框架层提供了访问接口,其中,在系统运行时库提供了C/C++调用接口,而在应用程序框架层提供了Java调用接口。Android开发中通常只需要调用Java接口,而实际上,应用程序框架层的Java调用接口是通过JNI方法来调用系统运行时库的C/C++调用接口,最后进入到内核空间的Ashmem驱动程序去的。
在Android中,主要提供了MemoryFile这个类来供应用使用匿名共享内存。在Android应用程序框架层,提供了一个MemoryFile接口来封装了匿名共享内存文件的创建和使用,通过JNI调用底层C++方法。
前面说到可以利用匿名共享内存来实现进程间内存共享,下面来具体讲解一下它的应用。
我们知道,使用Binder在进程间传递数据的时候,有时候会抛出TransactionTooLargeException这个异常,这个异常的产生是因为Binder驱动对内存的限制引起的。也就是说,我们不能通过Binder传递太大的数据。官方文档里有说明,最大通常限制为1M。
参见:https://developer.android.com/reference/android/os/TransactionTooLargeException.html
但是这个Binder的1M缓存是被当前进程中所有正在通过Binder传递数据的过程共享的,因此实际可用的大小要小于等于这个值。也就是说如果大于1M数据的话,就应该分开传。既然大数据不能通过Binder直接传递,我们就要想其他的办法。我们想到可以通过文件来跨进程传递数据,但是普通文件的效率太低,更优的方法是通过MemoryFile传递。
下面来讲一下这个方案怎么实现。考虑到进程间通信的场景,通常是Activity通过AIDL与Service通信,我们建立一个Activiy和Service,让他们分别跑在不同的进程。在Activiy端,具体的实现代码如下:

byte[] contentBytes = new byte[100];  
MemoryFile mf = new MemoryFile(“memfile”, contentBytes.length);  
mf.writeBytes(contentBytes, 0, 0, contentBytes.length);  
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");  
FileDescriptor fd = (FileDescriptor) method.invoke(mf);  
pfd = ParcelFileDescriptor.dup(fd);  

其中MemoryFile需要两个参数,第一个是文件名,这个可以为null,第二个参数是文件长度,然后调用writeBytes将数据写入文件。由于MemoryFile的getFileDescriptor方法是@hide的,需要用反射得到文件描述符。最后调用ParcelFileDescriptor.dup()的目的是将文件描述符序列化,这是因为通过Binder传递的数据必须都是可序列化的。这样文件描述符就通过Binder传到了Service进程中。
在Service端,通过AIDL拿到文件描述符后,通过正常的文件读取方式,就可以读取到数据。

fis = new FileInputStream(fd.getFileDescriptor());  
fis.read(new byte[100]);  

通过这样的方式,可以避免Binder对传递的数据过大的限制,又可以解决跨进程传递数据的效率问题。


关于Fresco在图片管理上的一些知识点

Purgeable Bitmap

Ashmem一般在应用层中是无法直接访问的,除了几个特例之外。其中之一就是 decode bitmap ,我们可以通过设置 BitmapFactory.Optinons.inPurgeable = true 来创建一个 Purgeable Bitmap ,这样decode出来的bitmap是在Ashmem内存中,GC无法自动回收它。当该Bitmap在被使用时会被 pin 住,使用完之后就 unpin ,这样系统就可以在将来某一时间释放这部分内存。

如果一个 unpinned 的bitmap在之后又要被使用,系统会运行时又将它重新decode,但是这个decode操作是发生在UI线程中的有可能会造成掉帧现象,因此改做法已经被Google废弃掉,转为鼓励使用 inBitmap 来告知bitmap解码器去尝试使用已经存在的内存区域,新解码的bitmap会尝试去使用之前那张bitmap在heap中所占据的pixel data内存区域,而不是去问内存重新申请一块区域来存放bitmap。利用这种特性,即使是上千张的图片,也只会仅仅只需要占用屏幕所能够显示的图片数量的内存大小。
但是使用 inBitmap 需要注意几个限制条件:
在SDK 11 -> 18之间,重用的bitmap大小必须是一致的,例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。 新申请的bitmap与旧的bitmap必须有相同的解码格式,例如大家都是8888的,如果前面的bitmap是8888,那么就不能支持4444与565格式的bitmap了。 我们可以创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。

Bitmap在Ashmem中的使用

Ashmem内存区域是不能被Java应用直接使用的,但这其中有一些例外,而Bitmap是其中一个.

BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);

Purgeable被设置成true以后,这个Bigmap就是保存在Ashmem内存区域中的,Java的垃圾回收是不能回收这篇区域的内存的.
当Android系统需要渲染这个Bitmap的时候,会调用pin,渲染完成后会调用unpin.而unpin后的内存空间表示能被其他人所使用.
如果被unpin的Bitmap需要重新渲染,系统会再次Decode这个Bitmap.而这个Decode的过程是在UI线程上完成的.所以Google后来废弃了这个pureable的参数.

后来Google提供了另外一个Flag,叫inBitmap.很遗憾的是,知道Android4.4后,这个新的Flag才得到完善.而Fresco致力于实现一个包括Android2.3以及以上的Android系统都能完美工作的图片加载管理开源库,因此Fresco放弃了使用inBitmap的解决方案.

Fresco是如何利用Ashmem去给Bitmap分配和管理内存?
MemoryFile memoryFile = new MemoryFile(null, inputStream.available());
memoryFile.allowPurging(false);
OutputStream outputStream = memoryFile.getOutputStream();
outputStream.write(1024);

上面可以看到allowPurging这个调用,这个就是之前说的”pin”和”unpin”,在设置了allowPurging为false之后,这个MemoryFile对应的Ashmem就会被标记成”pin”,那么即使在android系统内存不足的时候,也不会对这段内存进行回收。另外,由于Ashmem默认都是”unpin”的,因此申请的内存在某个时间点内都可能会被回收掉,这个时候是不可以再读写了.
上面说到的pin和unpin两个操作,对应的NDK调用是AndroidBitmap_lockPixels和unlockPixels.按照我们一惯认知,为了避免内存泄漏,这两者必须成对出现.而Fresco为了避免Bitmap再次渲染而导致的在UI线程Decode的过程,偏偏不在渲染完成后调用unlockPixels.
这做后,Fresco需要自己去管理这块内存区域,保证当这个Bitmap不再使用时,Ashmem的内存空间能被unpin.
而Fresco选择在Bitmap离开屏幕可视范围时候(onDetachWindow等时候),去做unpin.

你可能感兴趣的:(android,android,Fresco图片优化)