移动设备开发中,由于移动设备(手机等)的内存有限,所以使用有效的缓存技术是必要的。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(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(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {//计算每一个缓存的图片所占用内存大小
return bitmap.getRowBytes() * bitmap.getHeight();
}
};
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 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;
}
可以看到,put()方法主要有以下几步:
1)key和value判空,说明LruCache中不允许key和value为null;
2)通过safeSizeOf()获取要加入对象数据的大小,并更新当前缓存数据的大小;
3)将新的对象数据放入到缓存中,即调用LinkedHashMap的put方法,如果原来存在该key时,直接替换掉原来的value值,并返回之前的value值,得到之前value的大小,更新当前缓存数据的size大小;如果原来不存在该key,则直接加入缓存即可;
4)判断缓存空间是否已满。
我们看到上面的方法牵扯到线程安全的都加入了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方法。因为图片缓存时内存缓存没有命中会去文件缓存中去取或者从网络下载,所以并不需要创建。*/
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;
}
}
get()方法的思路就是:
1)先尝试从map缓存中获取value,即mapVaule = map.get(key);如果mapVaule != null,说明缓存中存在该对象,直接返回即可;
2)如果mapVaule == null,说明缓存中不存在该对象,大多数情况下会直接返回null;但是如果我们重写了create()方法,在缓存没有该数据的时候自己去创建一个,则会继续往下走,中间可能会出现冲突,看注释;
3)注意:在我们通过LinkedHashMap进行get(key)或put(key,value)时都会对链表进行调整,即将刚刚访问get或加入put的结点放入到链表尾部。
通过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。所以我们可以主动移除缓存中所缓存的数据。
下面看一下entryRemoved()的源码
我们发现entryRemoved方法是一个空方法,说明这个也是让开发者自己根据需求去重写的。entryRemoved()主要作用就是在结点数据value需要被删除或回收的时候,给开 发者的回调。开发者就可以在这个方法里面实现一些自己的逻辑:
(1)可以进行资源的回收;
(2)可以实现二级内存缓存,可以进一步提高性能,思路如下:重写LruCache的entryRemoved()函数,把删除掉的item,再次存入另外一个LinkedHashMap
entryRemoved()在LruCache中有四个地方进行了调用:put()、get()、trimToSize()、remove()中进行了调用。
上面就是整个LruCache中比较核心的的原理和方法,对于LruCache的使用者来说,我们其实主要注意下面几个点:
(1)在构造LruCache时提供一个总的缓存大小;
(2)重写sizeOf方法,对存入map的数据大小进行自定义测量;
(3)根据需要,决定是否要重写entryRemoved()方法;
(4)使用LruCache提供的put和get方法进行数据的缓存
小结:
LruCache 自身并没有释放内存,只是 LinkedHashMap中将数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存;
覆写 entryRemoved 方法能知道 LruCache 数据移除是是否发生了冲突(冲突是指在map.put()的时候,对应的key中是否存在原来的值),也可以去手动释放资源。
下面分享一个用于缓存下载的网络图片的cache实现:
public class BitmapCache implements ImageCache {
private static LruCache mCache;
public BitmapCache() {
if (mCache == null) {
// 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
// LruCache通过构造函数传入缓存值,以KB为单位。
int maxMemory = (int) (Runtime.getRuntime().maxMemory());
// 使用最大可用内存值的1/8作为缓存的大小。
int cacheSize = maxMemory / 10;
mCache = new LruCache(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的话,我们就需要重新在网络下载。