首页 > 代码库 > Android Bitmap 全面解析(二)加载多张图片的缓存处理

Android Bitmap 全面解析(二)加载多张图片的缓存处理

一般少量图片是很少出现OOM异常的,除非单张图片过~大~ 那么就可以用教程一里面的方法了
通常应用场景是listview列表加载多张图片,为了提高效率一般要缓存一部分图片,这样方便再次查看时能快速显示~不用重新下载图片
但是手机内存是很有限的~当缓存的图片越来越多,即使单张图片不是很大,不过数量太多时仍然会出现OOM的情况了~
本篇则是讨论多张图片的处理问题

-----------------------------------------------------------------------

图片缓存的一般处理
1.建立一个图片缓存池,用于存放图片对应的bitmap对象
2.在显示的时候,比如listview对应适配器的getView方法里进行加载图片的工作, 先从缓存池通过url的key值取,如果取到图片了直接显示,
如果获取不到再建立异步线程去下载图片(下载好后同时保存至图片缓存池并显示)

但是缓存池不能无限大啊~不然就会异常了,所以通常我们要对缓存池进行一定控制
需要有两个特性: 
     总大小有个限制,不然里面存放无限多的图片时会内存溢出OOM异常
     当大小达到上限后,再添加图片时,需要线程池能够智能化的回收移除池内一部分图片,这样才能保证新图片的显示保存


异步线程下载图片神马的简单,网上异步下载任务的代码一大堆,下载以后流数据直接decode成bitmap图片即可
难点在与这个图片缓存池的设计,现在网上的实现主要有两种
1.软引用/弱引用
2.LruCache

-----------------------------------------------------------------------

拓展: java中4种引用分类
官方资料连接:http://developer.android.com/reference/java/lang/ref/Reference.html
强引用
     平常使用的基本都是强引用,除非主动释放(图片的回收,或者==null赋值为空等),否则会一直保存对象到内存溢出为止~
软引用     SoftReference
     在系统内存不够时,会自动释放部分软引用所指对象~
弱引用     WeakReference
     系统偶尔回收扫描时发现弱引用则释放对象,即和内存够不够的情况无关,完全看心情~
虚引用
     不用了解,其实我也不熟悉

框架基本都比较爱用这个软应用保存图片作为缓存池,这样在图片过多不足时,就会自动回收部分图片,防止OOM
但是有缺点,无法控制内存不足时会回收哪些图片,如果我只想回收一些不常用的,不要回收常用的图片呢?


于是引入了二级缓存的逻辑
即设置两个缓存池,一个强引用,一个软引用, 强引用保存常用图片,软应用保存其他图片~
强引用因为不会自动释放对象,所以大小要进行一定限定,否则图片过多会异常, 比如控制里面只存放10张图片,
然后每次往里面添加图片的时候,检查如果数量超过10张这个阀值,临界点值时,
就移除强引用里面最不常用的那个图片,并将其保存至软应用缓存池中~


整个缓存既作为一个整体(一级二级缓存都是内存缓存~每次显示图片前都要检查整个缓存池中有没有图片)
又有一定的区分(只回收二级缓存软引用中图片,不回收一级缓存中强引用的图片~)




代码实现
软应用缓存池类型作为二级缓存: 

 

1 HashMap<String, SoftReference<Bitmap>> mSecondLevelCache = new HashMap<String, SoftReference<Bitmap>>();

 

强引用作为一级缓存,为了实现删除最不常用对象,可以用 LinkedHashMap<String,Bitmap> 类

LinkedHashMap对象可以复写一个removeEldestEntry,这个方法就是用来处理删除最不常用对象逻辑的
按照之前的设计就可以这么写:

1 final int MAX_CAPACITY = 10; // 一级缓存阈值
2 
3 // 第一个参数是初始化大小
4 // 第二个参数0.75是加载因子为经验值
5 // 第三个参数true则表示按照最近访问量的高低排序,false则表示按照插入顺序排序 
6 HashMap<String, Bitmap> mFirstLevelCache = new LinkedHashMap<String, Bitmap>( 
7         MAX_CAPACITY / 2, 0.75f, true) { 

 

 1 // eldest 最老的对象,即移除的最不常用图片对象
 2      // 返回值 true即移除该对象,false则是不移除
 3     protected boolean removeEldestEntry(Entry<String, Bitmap> eldest) { 
 4         if (size() > MAX_CAPACITY) {// 当缓存池总大小超过阈值的时候,将老的值从一级缓存搬到二级缓存 
 5             mSecondLevelCache.put(eldest.getKey(), 
 6                     new SoftReference<Bitmap>(eldest.getValue())); 
 7             return true; 
 8         } 
 9         return false; 
10     } 
11 };  

每次图片显示时即使用时,如果存在与缓存中,则先将对象从缓存中删除,然后重新添加到一级缓存中的最前端
会有三种情况
1.如果图片是从一级缓存中取出来的,则相当于把对象移到了一级缓存池的最前端(相当于最近使用的一张图片)~
2.如果图片是从二级缓存中取出来的,则会存到一级缓存池最前端并检测,如果超过阀值,则将最不常用的一个对象移动到二级缓存中
3.如果缓存中没有,那就网上下载图片,下载好以后保存至一级缓存中,同样再进行检测是否要移除一个对象至二级缓存中

-----------------------------------------------------------------------

结合现实例子理解下(如果以上逻辑了解可以跳过):
美国篮球,比如有一个最高水平的联赛NBA,还有一个次一级的联赛NBDL~
一级联赛NBA的排名按最近一次拿冠军的时间由近到远排列,
我们规定,每一季度比赛都要产生一个冠军,冠军可能是已有的任何一个队伍也可能是一个民间来的新队伍~
而当一个队伍获取冠军的时候就给他加到一级队伍NBA里~ 由于是最近一次拿冠军,所以加进去的时候也是排名第一

NBA作为最高水平,我们对数量是有限制的,所以每次有新冠军产生的时候我们都做一次检测,
如果队伍总数量超过20支,那么就移除排名最低即离上次获冠军时间最长的那个最差队伍.


如果每季度比赛拿冠军相当于一次图片使用操作,那上面三种情况就对应我们例子中的:
1.NBA的队伍拿冠军,相当于这个队伍排名变成了第一名~但NBA队伍总数不变,没有新加入来的
2.NBDL二级联赛拿冠军,则加入到NBA里面,且变成了第一名~由于NBA队伍相当于增加了一个,那就要检测一下是否超过20支并将最差成绩的挤到NBDL中
3.民间来大神了虐了全部的队伍拿了冠军,那直接加入NBA然后变成第一名,同样,检测NBA球队数量判断是否要挤出去一队

NBDL球队相当于软应用的二级缓存池, 不限定数量~ 多少都可以, 直到美国篮联维护全部NBA NBDL球队的资金不够了(相当于图片过多应用内存不足了)
则自动解散一部分球队,落入民间,直到下一次获取总冠军再加入进来(相当于图片从缓存中移除了,下次使用要重新下载)~
那NBA就相当于一级缓存,经常拿冠军(相当于高频率使用的图片),那我们就不想因为资金不足随机解散几个球队恰好就解散了NBA队伍,
则规定资金不够时只解散二级联赛NBDL的队伍~因为他们获取比赛几率低一点~

民间队伍存在与联赛系统之外(相当于不存在缓存中的图片), 而任何一个NBA NBDL联赛球队我们都可以理解为都是民间晋级过来的~
只不过从民间获取总冠军并加入联赛需要一个取名字啊登记啊等等的办手续过程(下载图片),比较麻烦,所以我们要尽可能的少办手续~
而联赛队伍(包括NBA NBDL)获取总冠军则不需要麻烦的手续,可以直接参加比赛去拿冠军(直接获取显示)


两个联赛,一个常用的限定数量,一个不常用的不限定数量,但是资金不足时自动回收部分二级球队~
相当于图片的二级缓存

-----------------------------------------------------------------------

Disk缓存
可以简单的理解为将图片缓存到sd卡中~

由于内存缓存在程序关闭第二次进入时就清空了,对于一个十分常用的图片比如头像一类的~
我们希望不要每次进入应用都重新下载一遍,那就要用到disk缓存了,直接图片存到了本地,打开应用时直接获取显示~

网上获取图片的大部分逻辑顺序是
内存缓存中获取显示(强引用缓存池->弱引用缓存池)  -> 内存中找不到时从sd卡缓存中获取显示 -> 缓存中都没有再建立异步线程下载图片,下载完成后保存至缓存中

按照获取图片获取效率的速度,由快到慢的依次尝试几个方法

以文件的形式缓存到SD卡中,优点是SD卡容量较大,所以可以缓存很多图片,且多次打开应用都可以使用缓存,
缺点是文件读写操作会耗费一点时间,
虽然速度没有从内存缓存中获取速度快,但是肯定比重新下载一张图片的速度快~而且还不用每次都下载图片浪费流量~
所以使用优先级就介于内存缓存和下载图片之间了

注意:
sd卡缓存一般要提前进行一下是否装载sd卡的检测, 还要检测sd卡剩余容量是否够用的情况
程序里也要添加注明相应的权限

-----------------------------------------------------------------------

使用LruCache处理图片缓存

以上基本完全掌握了,每一张图最好再进行一下教程(一)里面介绍的单张缩放处理,那基本整个图片缓存技术就差不多了
但随着android sdk的更新,新版本其实提供了更好的解决方案,下面介绍一下

先看老版本用的软引用官方文档
http://developer.android.com/reference/java/lang/ref/SoftReference.html
摘取段对软引用的介绍
Avoid Soft References for Caching

In practice, soft references are inefficient for caching. The runtime doesn‘t have enough information on which references to clear and which to keep. Most fatally, it doesn‘t know what to do when given the choice between clearing a soft reference and growing the heap.
The lack of information on the value to your application of each reference limits the usefulness of soft references. References that are cleared too early cause unnecessary work; those that are cleared too late waste memory.

Most applications should use an android.util.LruCache instead of soft references. LruCache has an effective eviction policy and lets the user tune how much memory is allotted.


简单翻译一下

我们要避免用软引用去处理缓存 

在实践中,软引用在缓存的处理上是没有效率的。运行时没有足够的信息用于判断哪些引用要清理回收还有哪些要保存。

最致命的,当同时面对清理软引用和增加堆内存两种选择时它不知道做什么。  
对于你应用的每一个引用都缺乏有价值的信息,这一点限制了软引用让它的可用性十分有限。
过早清理回收的引用导致了无必要的工作; 而那些过晚清理掉的引用又浪费了内存。

大多数应用程序应该使用一个android.util。LruCache代替软引用。LruCache有一个有效的回收机制,让用户能够调整有多少内存分配。



简而言之,直接使用软引用缓存的话效果不咋滴~推荐使用LruCache
level12以后开始引入的,为了兼容更早版本,android-support-v4包内也添加了一个LruCache类,
所以在12版本以上用的话发现有两个包内都有这个类,其实都是一样的~

那么这个类是做啥的呢~这里是官方文档
http://developer.android.com/reference/android/util/LruCache.html

LRU的意思是Least Recently Used 即近期最少使用算法~
眼熟吧,其实之前的二级缓存中的那个强引用LinkedHashMap的处理逻辑其实就是一个LRU算法
而我们查看LruCache这个类的源代码时发现里面其实也有一个LinkedHashMap,大概扫了眼,逻辑和我们之前自己写的差不多
核心功能基本都是: 当添加进去新数据且达到限制的阀值时,则移除一个最少使用的数据


根据这个新的类做图片加载的话,网上大部分的做法还是二级缓存处理
只不过将LinkedHashMap+软引用
替换成了LruCache+软引用
都是二级缓存,强引用+软引用的结构~因为LruCache和LinkedHashMap都是差不多的处理逻辑
没有移除软引用的使用,而是将两者结合了起来

根据官网的介绍来看其实软引用效果不大,二级缓存的处理的话,虽然能提高一点效果,但是会浪费对内存的消耗~
所以要不要加个软引用的二级缓存,具体选择就看自己理解和实际应用场景了吧

LruCache我理解是牺牲一小部分效率,换取部分内存~我个人也是倾向于只使用LruCache的实现不用软引用了,也比较简单~


结合前面举得例子可以理解为,直接取消NBDL二级联赛(软引用)~ 这样能省下好大一笔钱(内存)然后投资联赛其他方面(处理其他逻辑)
并扩展下NBA一级联赛(LruCache)的规模~保证复用效率

-----------------------------------------------------------------------

LruCache的具体用法

之前对LinkedHashMap有了一定了解了,其实LruCache也差不多
类似于removeEldestEntry方法的回收逻辑,在这个类里面已经处理好了
一般我们只需要处理对阀值的控制就行了

阀值控制的核心方法是sizeOf()方法, 该方法的意思是返回每一个value对象的大小size~
默认返回的是1~即当maxSize(通过构造方法传入)设为10的时候就相当于限制缓存池只保留10个对象了~
和上面LinkedHashMap的例子一个意思


但是由于图片的大小不一,一般限定所有图片的总大小更加合适,那我们就可以对这个sizeOf方法进行复写

1 @Override
2 protected int sizeOf(String key, Bitmap value) {
3      return value.getRowBytes() * value.getHeight();
4 }

这样的话,相当于缓存池里每一个对象的大小都是计算它的字节数,则在新建LruCache的时候传入一个总size值就行了,
一般传入应用可用内存的1/8大小

-----------------------------------------------------------------------

本篇是讨论对于图片数量的控制问题, 再结合教程(一)中的方法对每一张图片进行相应处理~OOM的情况基本就可以避免了~