移动设备开发中,由于移动设备(手机等)的内存有限,所以使用有效的缓存技术是必要的。android提供来一个缓存工具类LruCache,开发中我们会经常用到,下面我们就具体分析一下LruCache。
LruCache缓存数据是采用持有数据的强引用来保存一定数量的数据的。每次用到(获取)一个数据时,这个数据就会被移动(一个保存数据的)队列的头部,当往这个缓存里面加入一个新的数据时,如果这个缓存已经满了,就会自动删除这个缓存队列里面最后一个数据,这样一来使得这个删除的数据没有强引用而能够被gc回收。
1. 首先我们先看一下LruCache的构造函数:
public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; this.map = new LinkedHashMap<K, V>(0, 0.75f, true); }
1.1 创建LruCache时需要传入一个maxSize,这表示LruCache规定的最大存储空间。这里我想问一下maxSize是指缓存数据对象的个数呢,还是缓存数据所占用的内存总量呢?
其实都可以,可以是缓存数据的个数,也可以使缓存数据所占用内存总量,当然也可以是其他.到底是什么,需要看你的LruCache如何重写这个方法:sizeOf(K
key, V value),我们看一下LruCache的sizeOf函数源码:
protected int sizeOf(K key, V value) {//子类覆盖这个方法来计算出自己的缓存对于每一个保存的数据所占用的量 return 1;//默认返回1,这说明:默认情况下缓存的数量就是指缓存数据的总个数(每一个数据都是1). }
那如果我使用LruCache来保存bitmap的图片,并且希望缓存的容量是4M那这么做,参考代码如下:
int cacheSize = 4 * 1024 * 1024; // 4MiB new LruCache<String, Bitmap>(cacheSize) { <span style="white-space:pre"> </span>@Override <span style="white-space:pre"> </span>protected int sizeOf(String key, Bitmap bitmap) {//计算每一个缓存的图片所占用内存大小 <span style="white-space:pre"> </span>return bitmap.getRowBytes() * bitmap.getHeight(); <span style="white-space:pre"> </span>} };
1.2 LruCache构造函数中我们注意到了LinkedHashMap这个类,我们知道LinkedHashMap是保存一个键值对数据的,并且可以维护这些数据相应的顺序的,LinkedHashMap初始化的源码如下:
//调用HashMap的构造方法来构造底层的数组 public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; //链表中的元素默认按照插入顺序排序 }
其中initialCapacity表示加载加载因子,在HashMap扩容时会用到。另外一个参数就是loadFactor,LinkedHashMap内部维持了一个双向循环链表,链表的排序有两种,用loadFactor参数的值进行区分,当loadFactor为false时,表示按照插入顺序排序,当loadFactor为true时,标志按照访问顺序排序。LruCache中LinkedHashMap的构造函数传入了true,这实现保存的数据是有一定顺序的,它是按访问顺序排序排序的,使用过一个存在的数据,这个数据就会被移动到数据队列的头部。
想要具体了解LinkedHashMap,请参见LinkedHashMap源码剖析:http://blog.csdn.net/ns_code/article/details/37867985
2. LruCache如何、何时判断是否缓存已经满来,并且需要移除不常用的数据呢?
在LruCache里面有一个方法:trimToSize()就是用来检测一次当前是否已经满,如果满来就自动移除一个数据,一直到不满为止:
public void trimToSize(int maxSize) {//默认情况下传入是上面说的最大容量的值 this.maxSize while (true) {//死循环.保证一直到不满为止 K key; V value; synchronized (this) {//线程安全保证 if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + .sizeOf() is reporting inconsistent results!); } if (size <= maxSize) {//如果不满,就跳出循环 break; } Map.Entry<k, v=""> toEvict = map.eldest();//取出最后的数据(最不常用的数据) if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key);//移除这个数据 size -= safeSizeOf(key, value);//容量减少 evictionCount++;//更新自动移除数据的数量(次数) } entryRemoved(true, key, value, null);//用来通知这个数据已经被移除,如果你需要知道一个数据何时被移除你需要从写这个方法entryRemoved } }
上面的源码中我给出了说明,很好理解。这里要注意的是trimToSize这个方法是public的,说明其实我们自己可以调用这个方法的。那我trimToSize这个方法何时调用呢?
trimToSize这个方法在LruCache里面多个方法里面会被调用来检测是否已经满了,比如在往LruCache里面加入一个新的数据的方法put里面,还有在通过get(K
key)这个方法获取一个数据的时候等,都会调用trimToSize来检测一次。
3. 下面看看LruCache的put方法
put方法是向LruCache缓存中添加一条新数据:
public final V put(K key, V value) {//加入一个新的数据 if (key == null || value == null) { throw new NullPointerException(key == null || value == null); } V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); //size加上预put对象的大小 previous = map.put(key, value); if (previous != null) { //如果之前存在键为key的对象,则size应该减去原来对象的大小 size -= safeSizeOf(key, previous); } } if (previous != null) {//加入重复位置的数据,则移除老的数据 entryRemoved(false, key, previous, value); } trimToSize(maxSize);//每次新加入对象都需要调用trimToSize检测缓存的数据是否已经满 return previous; }
我们看到上面的方法牵扯到线程安全的都加入了synchronized关键字,由此可见LruCache就是线程安全的。
4. 下面看看 LruCache的get方法:
Get方法通过key返回相应的item
我们看一下get的源码:
public final V get(K key) {//获取一个数据 if (key == null) { throw new NullPointerException(key == null); } V mapValue; synchronized (this) { mapValue = map.get(key); if (mapValue != null) {//取得这个数据 hitCount++;//成取得数据的次数 return mapValue;//成功取得这个数据 } missCount++;//取得数据失败次数 } /*如果未命中,则试图创建一个对象,这里方法返回null,并没有实现创建对象的方法如果需要事项创建对象的方法可以重写create方法。因为图片缓存时内存缓存没有命中会去<span style="font-family: Arial, Helvetica, sans-serif;">文件缓存中去取或者从网络下载,所以并不需要创建。*/</span> V createdValue = create(key);//尝试创建这个数据 if (createdValue == null) { return null;//创建数据失败 } synchronized (this) {//加入这个重新创建的数据 createCount++;//从新创建数据次数 mapValue = map.put(key, createdValue); if (mapValue != null) { //如果mapValue不为空,则撤销上一步的put操作。 map.put(key, mapValue); } else { size += safeSizeOf(key, createdValue); } } if (mapValue != null) { entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { trimToSize(maxSize);//检测是否满 return createdValue; } }
通过key返回相应的item,或者创建返回相应的item。相应的item会移动到队列的头部,如果item的value没有被cache或者不能被创建,则返回null。
我们在上面源码中看到了一个create(key)方法,我们看一下create方法的源码:
protected V create(K key) { return null; }
可以看到源码默认返回了一个null,我们分析上面get的代码,
V createdValue = create(key);//尝试创建这个数据
if (createdValue == null) {
return null;//创建数据失败
}
代码中判断如果create返回空,get也就返回空,表示所查询的不再cache缓存中,在文件缓存中找不到的话就会重新在网络上下载。
那么create一直都会返回null,LruCache中为什么要creat方法呢,我们分析一下如果create不反悔NULL的话会怎么处理,看源码可知他将返回的createdValue添加到map集合中了,然后再将createdValue返回给用户。从上面的分析可以知道,虽然源码中creat方法返回横为NULL,可是我们可以重写create方法来重新创建已经不存在的数据。当然一般情况下不需要这样做,当查找不到相应缓存时会重新从网络上下载。
5. 最后再看看remove方法:
/** * 删除key相应的cache项,返回相应的value */ public final V remove(K key) { if (key == null) { throw new NullPointerException("key == null"); } V previous; synchronized (this) { previous = map.remove(key); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, null); } return previous; }
Remove方法是删除key相应的cache项,返回相应的value。所以我们可以主动移除缓存中所缓存的数据。
介绍到这里,LruCache的主要代码就介绍完毕了,
下面分享一个用于缓存下载的网络图片的cache实现:
public class BitmapCache implements ImageCache { private static LruCache<String, Bitmap> mCache; public BitmapCache() { if (mCache == null) { // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 // LruCache通过构造函数传入缓存值,以KB为单位。 int maxMemory = (int) (Runtime.getRuntime().maxMemory()); // 使用最大可用内存值的1/8作为缓存的大小。 int cacheSize = maxMemory / 10; mCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes() * bitmap.getHeight(); } }; } } @Override public Bitmap getBitmap(String url) { return mCache.get(url); } @Override public void putBitmap(String url, Bitmap bitmap) { mCache.put(url, bitmap); } }
在应用中就可以通过getBitmap、putBitmap来获取或添加图片数据了。当调用getBitmap返回的是null的话,我们就需要重新在网络下载。