fresco是facebook主导的一个开源图片缓存库,从它提供的示例程序comparison来看,fresco貌似比其他的几个目前android主流的图片缓存库(glide,picasso,uil等)更快更节省内存。接下来就看看它到底是如何做到的。注:本文分析基于0.8.1版本。
背景知识
- lru与SoftReference那些年的爱恨情仇:很久很久以前,android的内存缓存还用SoftReference,在2.3以后,垃圾回收机制被修改,软引用已不推荐用作缓存技术了,lru等才是正义。既然软引用已经不再推荐使用,为毛还要提到它呢?因为freso偏偏用到了它!
- dalvik vm heap和native heap:heap即为堆,malloc、new等函数(方法)都是从堆申请内存空间,这点不用多解释。两个heap的区别在于,java操作的是dvm的heap,native代码使用malloc操作的是native heap。而dvm对heap的大小是有限制的,如果超过了这个限制,就会抛出OOM异常。这也是OOM异常的由来。于是fresco就会在native层申请内存来代替部分java层的内存。
- ashmem:匿名共享内存,前面提到fresco会用native申请内存空间,ashmem就是其中的一种。ashmem通过pin、unpin操作内存块,被unpinned的内存会放在一个lru列表,当内核空间不足时调用ashmem注册的回调来清理内存。具体介绍可参考Android系统匿名共享内存Ashmem(Anonymous Shared Memory)驱动程序源代码分析 。
- Bitmap:关于Bitmap的使用技巧很多,官方有详细的文档,这里只提一下fresco用到的部分内容。
inBitmap,传入一个Bitmap给BitmapFactory复用,减少申请内存。关于不同版本对inBitmap的大小限制见官方文档,fresco取的是大小一致的,能满足最低条件。
inTempStorage,在BitmapFactory对流进行解码时会分配一段临时的缓冲,如果不传入的话,每次解码都会重新申请,官方建议是16k。
inPurgeable,根据官方文档,如果这个参数被设置为true,Bitmap会创建一段pixels空间,当系统需要回收时,这段空间就会被清除。在这种情况下,如果需要重新访问这段空间,Bitmap就会重新对未编码的数据进行编码。这未编码数据的保存方式可持有它的引用或进行一次全拷贝,由inInputShareable参数控制。这两个参数都仅限lollipop以下版本使用。这个参数具体是如何影响内存管理的,涉及到skia的具体实现,从BitmapFactory的相关代码来看,貌似也用到了ashmem,有兴趣的可自行研究。
inJustDecodeBounds,这个参数应该比较常见了,一般用在对图片压缩。如果直接对一张大尺寸的图片进行解码,会十分占用内存,可以要求BitmapFactory按照固定的大小进行编码。这时我们可以通过这个参数先进行一次解码,只获取图片的大小,并不解析其中的数据,然后将图片大小按照一定比例进行缩放,再通过BitmapFactory来真正解码。
重要类的结构
fresco有几类比较核心的内容,包括cahce、pool、decoder以及producer和consumer。下面分别介绍。
1. cache,这明显就是缓存了,其中包括内存缓存及磁盘缓存,而内存缓存又派生了别的几个子类。
![](https://img-blog.csdn.net/20151105002303231)
所有的内存缓存都实现了MemoryCache接口。InstrumentedMemoryCache和AnimatedFrameCache的实际操作都由它们的代理CountingMemoryCache来执行。内存缓存还可以分为已解码的缓存和未解码的缓存,如将一张图片读到内存后得到Encoded数据存入未解码缓存,然后对其解码,得到Bitmap存入已解码的缓存。可见fresco的缓存是多种多样的。
2. pool,其实也可以说是缓存,里面保存的对象基本都是申请的内存空间,将这些对象实例存起来就是为了要复用其申请的内存。cache里的MemoryCache缓存的东西是可以直接拿来用的,当前不用再对里面的数据进行操作,而pool只是复用了对象实例的空间,从pool取出来马上就要对其中的空间进行数据写入等操作。总之,它们的区别在于,MemoryCache复用数据,而pool仅是复用其内存空间。
![](https://img-blog.csdn.net/20151105002353941)
整个对象池家族中最基础的就是Pool接口,BasePool实现了Pool,然后从它们派生出了各种各样的对象池。BasePool还包含了一个Bucket的SparseArray,所有需要缓存的实体,属性一样的都存在同一个Bucket中。这里所说的属性,对应内存复用而言,自然是大小了。被缓存的对象包括byte数组、NativeMemoryTrunk等。图中以NativeMomoryTrunk为例,画出了它们之间的关系。下面简单介绍一下几个重要的类。
NativMemoryTrunk,看名字就能猜出它是用到了native的内存空间,这与我们提到的第2点背景知识相符,主要是用来存放Encoded数据。
OOMSoftReference,背景知识第一点就提到了软引用,fresco到底用了什么黑科技?可以看到,OOMSoftReference有三个SoftReference,其中的注释说到,每次dvm的gc时,每第二个软引用都会被视为弱引用被回收掉。这也就意味着只要我们在(dvm)heap里有两个连续的软引用就能保证其中一个不会被回收,也就意味着它们所引用的对象不会被回收。然而我们无法保证软引用在heap里的位置,为了安全起见,于是就连续new了三个实例出来。关于dvm对软引用的回收可以看MarkSweep的preserveSomeSoftReferences方法:
<code class="hljs php has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> void preserveSomeSoftReferences(Object **<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">list</span>)
{
assert(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">list</span> != <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">NULL</span>);
GcMarkContext *ctx = &gDvm.gcHeap->markContext;
size_t referentOffset = gDvm.offJavaLangRefReference_referent;
Object *clear = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">NULL</span>;
size_t counter = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>;
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">while</span> (*<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">list</span> != <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">NULL</span>) {
Object *ref = dequeuePendingReference(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">list</span>);
Object *referent = dvmGetFieldObject(ref, referentOffset);
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (referent == <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">NULL</span>) {
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/* Referent was cleared by the user during marking. */</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">continue</span>;
}
bool marked = isMarked(referent, ctx);
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (!marked && ((++counter) & <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>)) {
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/* Referent is white and biased toward saving, mark it. */</span>
markObject(referent, ctx);
marked = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">true</span>;
}
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> (!marked) {
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/* Referent is white, queue it for clearing. */</span>
enqueuePendingReference(ref, &clear);
}
}
*<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">list</span> = clear;
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/*
* Restart the mark with the newly black references added to the
* root set.
*/</span>
processMarkStack(ctx);
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li></ul>
注意其中的if(!marked && ((++counter) & 1))这个条件,只要当它成立的时候才会标记该引用。没有被标记的软引用就会被回收掉了。
FlexByteArrayPool,Kitkat版本的decoder使用,具体操作由SoftRefByteArrayPool实现,用来缓存未解码的(Encoded)图片数据。关于deocder见第3点介绍。
SynchronizedPool,ArtDecoder使用,缓存背景知识第4点提到inTempStorage所使用的一段临时缓冲。它使用Object数组来保存对象实例,并未使用Bucket。
BitmapPool,同样也是ArtDecoder使用,缓存背景知识第4点inBitmap提到的重用Bitmap。
NativeMemoryTrunkPool,缓存NativeMemoryTrunk的对象池。
3. decoder,用来解码的类,根据不同平台有不同的实现,主要分为3种decoder。其中Gingerbread和Kitkat都继承自DalvikPurgeableDecoder,这跟背景知识第四点提到的inPurgeable有关。
![](https://img-blog.csdn.net/20151105002426272)
GingerbreadPurgeableDecoder,Gingerbread <= sdk < KitKat时使用,Encoded数据保存在native层(NativeMemoryTrunk),并复制到MemoryFile(ashmem),之后交给bitmap解码,现在默认配置不会启用。从github上的某条commit来看,貌似会导致OOM所以不用了。
KitKatPurgeableDecoder,sdk < Lollipop,Encoded数据保存在native层(NativeMemoryTrunk),从FlexByteArrayPool获取一段java的byte数组缓冲,将Encoded数据复制过去,之后由bitmap进行解码。它使用byte数组代替ashmem,从代码的注释来看,fresco用到的ashmem的技巧貌似从kitkat之后就不好使了,具体是什么原因还没看出来。现在已经代替GingerbreadPurgeableDecoder。
ArtDecoder,sdk >= Lollipop,直接将Encoded数据封装为流来解码,解码时用到了ByteBuffer缓冲(SynchronizedPool)。
4. producer和consumer,跟操作系统或者多线程的生产者消费者模式的关系貌似并不是特别大。fresco获取图片就是不断的把自己定义为一个consumer,然后向相应的producer请求数据(produceResults)的过程。以从磁盘缓存获取图片为例,首先向内存缓存的producer请求数据,内存缓存发现找不到,然后自己也定义一个consumer,向磁盘缓存producer请求数据,此时磁盘缓存找到了目标图片,然后回调内存缓存的consumer,此时内存缓存把图片缓存起来,最后回调那个调用内存缓存producer的consumer。当然了,实际的过程会更加复杂,这里已经把解码的过程省略了。
获取图片的过程
上面第四点已经大致提了一下流程,下面我们已时序图的方式来看一下更加详细的调用过程,需要说明的是,下图展示的仍不是完成的流程,为了看起来更加简洁,省略了部分非业务流程。我们假设平台为KitKat,图片要从磁盘缓存中读取,并且该图片是jpg格式的。其他几种情况可自行研究。
![](https://img-blog.csdn.net/20151105002501195)
(1) 从内存缓存中取图片
(2)-(3) 内存缓存中没有,再向未编码的内存缓存取。注意这两个缓存使用的都是InstrumentedMemoryCache,实际上它们的对象实例是不一样的,已解码的存的是Bitmap类型的,未解码的存的是EncodedImage类型的。
(4)-(5) 从磁盘缓存中取数据
(6) 磁盘缓存中有,把数据读到内存来
(7) 申请一段NativeMemoryChunk内存,把磁盘的数据读过去
(8) 回调EncodedMemoryProducer定义的Consumer,这里先会对得到的数据进行缓存,然后复制一个新的EncodedImage继续传递
(9) 将数据保存到未解码的缓存中
(10) 回调ResizeAndRotateProducer的Consumer,判断是否需要缩放或旋转,这里假设不需要
(11) 回调DecodeProducer的Consumer,它会向其工作队列中提交一个解码任务
(12) 获取解码的一些参数,如解码的质量等
(13) 根据图片格式执行不同的解码方法,这里假设是jpg
(14) 直接调用对应平台的解码方法,这里假设是Kitkat
(15) 从FlexByteArrayPool中获取一段byte数组缓冲,将图片Encoded数据从NativeMemoryChunk中复制过去,解码得到Bitmap
(16) 回调BitmapMemoryCacheProducer的Consumer
(17) 将解码后的Bitmap放入缓存中
总结
如果不算ashmem的purgable,fresco还用到了三个缓存,首先是DiskCache,然后还有两个MemoryCache,分别是保存已解码Bitmap的和保存EncodedImage的缓存。除此之外,为了避免频繁的申请内存、回收内存造成内存抖动,fresco还用到了大量的对象池。相关的缓存技术有lru、ashmem、SoftReference等,已经深入到了虚拟机层面。
遗留问题:
1. 关于为什么在Kitkat之后放弃使用ashmem来暂存待解码的数据,这点还没弄清楚,尝试在5.0版本的真机上使用GingerbreadPurgeableDecoder来解码,没发现有什么问题。
2. 为什么在Lollipop之前的版本都不设置inBitmap参数,这跟inPurgeable有什么冲突吗?稍微看了一下glide的源码,貌似它在Kitkat之前的版本也照样使用inBitmap参数的。
转载自:http://blog.csdn.net/sgwhp/article/details/49640611