1、LruCache概述:
位于android.util包中,是在Android3.1(Honeycomb MR1)中加入的。在之前的版本中,可以通过supprot包使用。
LruCache的本质仍然是一个缓存区,持有对“值”的强引用。其特色在于缓存淘汰算法使用的是LRU算法。
LRU(Least recently used,最近最少使用)算法是根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
当每次命中一个“值”的时候,会将这个“值”移动到链表头部。如果队列已满,会抛弃链表尾部的“值”,使其能被垃圾回收器在合适的时间点回收掉。
2、LruCache的实现:
首先看下它的成员变量:
private int size; //已用大小
private int maxSize;//容量
private int putCount; //存放次数
private int createCount; //为key创建value的次数
private int evictionCount; //抛弃最老的value的次数
private int hitCount; //命中次数
private int missCount; //未命中次数
然后是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);
}
从最后一行可以看到,其底层使用的LinkedHashmap。map的初始容量为0,加载因子为0.75,最后一个参数true表明它是基于访问排序的,也就是说当get到一个“值”后,这个“值”就会移动到尾部。如下:
首先放入四个值
map.put(1, “a”);
map.put(2, “b”);
map.put(3, “c”);
map.put(4, “d”);此时链表中的顺序是:a -> b -> c -> d
然后访问前两个值
map.get(1);
map.get(2);之后链表的顺序就变成了:c -> d -> a -> b
还真有点Lru的感觉。
接下来看它的put方法:
/**
* 根据键来缓存值,并将值移动到表头
*
* @return 返回这个键之前关联的值
*/
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous; //用来保存key之前关联的值
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
//获取key之前对应的value,如果之前没有value,那么便是null
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
//空函数
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
可以看到首先进行了合法性检查,其不允许null作为键或值(LinkedHashmap是允许键或值为null的)。
接下来同步锁定整个类,并完成以下操作:
- 存放次数putCount增加;
- 计算新值的大小,并更新已用大小;
- 将新值存放进去(如果键关联着旧值,那么同时删掉旧值);
- 如果旧值存在的话,当然需要更新size(减去旧值的大小)。
至此,存放新值和删除旧值操作已经完成。至于后面的entryRemoved其实是一个空方法,我们重写后便可以来定制自己的逻辑。
最后最后,调用trimToSize,检查有没有超过最大容量,如果超过了,那就扔掉最老的那个。扔掉后再检查再扔,直到小于等于最大容量。
再看一下它的get方法:
/**
* 如果键对应的值存在或者这个值能被创建出来的话,返回这个值
* 如果这个值被返回了,那么它将会被移动到链表头部
* 如果没找到而且不能被创建的话,返回null
*/
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++;
}
/*
* 尝试为此键去创建一个对应的值 。
* 创建的过程可能很耗时,即当创建完成时可能map已经改变了。
* 所以如果在create的过程中向map中放入了与此键对应的值,
* 我们会放弃创建的值,使用存进来的值。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last 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;
}
}
//默认返回为null的create函数:
protected V create(K key) {
return null;
}
首先仍是合法性检查。
接着加锁、根据key去获取value。
如果取到了,命中次数加1,返回value,执行完毕;
如果没取到,未命中次数加1,然后去尝试创建一个值。在源码中,create函数是一个空函数,直接返回null。而get函数发现创建的值为null,也直接返回null,执行完毕。
如果我们重写了create(K key)函数,使其不返回null,那么会继续往下执行:
首先是创建次数增加,然后将新的值存放进去。
如果存放的时候发现又有值了(因为创建的过程可能很耗时,在这期间我们又向里面插入了此键与一个值),那么便将这个值放进去,同时抛弃创建的值。最后将这个值返回,执行完毕。
如果创建、存放一切正常,那么修正已用大小,然后调用trimToSize清理下超出容量的缓存,最后返回这个创建的值。
最后看一下trimToSize方法:
/**
* 移除掉最老的键值对,直到已用大小<=传入的最大容量
*
* @param maxSize 函数执行完毕后期望最大容量
* 如果是-1的话代表清空整个map
*/
public void trimToSize(int 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);
}
}
函数内部是一个while循环,退出的条件是已用大小小于传入的最大容量,不然的话就扔掉最老的那一个键值对,同时抛弃次数加1,更新已用大小。
如果传入的maxSize是-1的话,意味着会一直抛弃最老的实体,同时调用entryRemoved方法,直到map为空。所以里面有一个清空缓存区的函数:
/**
* Clear the cache, calling entryRemoved on each removed entry.
*/
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
3、LruCache的分析:
(1)在LruCache的源码中,并没有发现关于LRU算法的具体实现。这是因为其实现主要位于LinkedHashMap中,即如果我们创建LinkedHashMap时使用三个参数的构造方法、并且最后一个参数传入true,那么就使用了LRU进行缓存抛弃算法。所以,具体的还是得看看LinkedHashMap源码。我会继续的。
(2)源码中有两个好玩的函数:
void entryRemoved(boolean evicted, K key, V oldValue, V newValue){}
V create(K key) {}
entryRemoved是在移除元素后调用,第一个参数代表是不是被抛弃,true代表调用时机为删除最老的元素,false代表更新key对应的value是移除了就的value。
create是在get方法未命中后调用的。
重写这两个方法能使根据我们的需要定制自己的逻辑,还是很棒的。