缓存虽然会占据一定的应用内存或者磁盘空间,却允许你以更快的速度获得你需要的对象。因此缓存避免了重复加载,可以提高应用的响应速度。Android中提供了LruCache类来支持缓存。这篇文章着重于对LruCache源码的分析。
类的介绍
LruCache持有一定数量对象的引用来避免这些对象被回收。当一个对象被访问时,将会被移到缓存队列的头部;当一个对象添加到已经满的队列时,队列的尾部对象(最不常访问)的对象将被踢出队列,意味着允许被垃圾回收器所回收。
如果在缓存队列中存在一些需要显性释放资源的对象时,需要重写entryRemoved方法。
如果缓存miss时,需要根据相应的key来产生对应的value时,需要重写create方法。
默认地,cache大小为缓存队列中的元素个数。然而,在实际运用中,cache大小与缓存队列中的对象大小有关。因此,需要重写sizeOf来改变这一点。
此类是线程安全的,通过synchronized关键字来限制put、get、remove操作。
LruCache不允许key或者value为空,否则将抛出异常-NullPointerException(“key == null”)。
变量介绍
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size;
private int maxSize;
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
map为缓存队列,LinkedHashMap内的队列有两种维护顺序的方式,一种是根据插入的顺序,新插入的元素将在尾部;另外一种是根据访问的顺序,新访问的元素将在尾部。LruCache中的LinkedHashMap使用的是根据访问顺序来实现LRU(最近最少访问)的。
size不是缓存队列中元素的个数,而是缓存队列中元素的大小,例如一个Bitmap所占的字节数;maxSize是缓存的最大空间。
putCount记录了队列中添加元素的次数;有时,缓存miss时,需要根据相应的key来产生value,createCount负责统计次数;evictionCount则是记录了踢出队列元素的个数;hitCount和missCount分别表示缓存hit和miss的次数。
构造函数
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
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);
}
如果没有重写sizeOf方法,maxSize指的是队列中元素的最大数目;否则,maxSize指的是cache的最大空间。
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
第二个参数指的是装载因子,指的是当缓存队列中的元素已经到达75%时,需要再散列;第三个参数传入true表示将以访问顺序维护队列,而传入false表示以插入顺序。
trimToSize
private 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);
}
}
trimToSize(int maxSize)方法是用来限制缓存队列大小到maxSize的方法。从代码中可以看得出,如果现在的size大于maxSize的话,将移除最不常访问的。当参数传入-1时,表示移除所有元素。
get方法
/**
* 如果存在或者能通过create方法生成的话,返回key对应的value
* 如果有value返回的话,它将会被移动到缓存队列的尾部.
* 当缓存miss并且不能通过create方法生成时,返回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++;
}
/*
* 尝试生成value,这可能会耗费一定的时间,在这段时间内,map可能有所变化,因此同一个key可能有
* 冲突,如果冲突发生,我们将遗弃通过create生成的value
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// 有冲突,将mapValue重新存入
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
// 显性释放资源
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 由于put了createdValue,验证是否大小超过maxSize
trimToSize(maxSize);
return createdValue;
}
}
从代码中,我们可以知道:
如果缓存队列中存在此key的话,直接返回key相应的value;
如果不存在的话,会尝试通过create方法来创建此value。create方法前面已经说过,就是如果缓存miss时,需要根据相应的key来产生对应的value时,需要重写create方法。由于create方法可能消耗一定的时间,因此在create返回之前,map内可能已经有这个key了,这时就会发生冲突,因此我们会遗弃create产生的value。
put方法
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);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
put方法中,如果key或value为空,返回空指针异常。如果原本有key存在,需要显性的释放资源。添加成功后,需要调用trimToSize方法避免size超过最大大小。
remove方法
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;
}
这个方法也比较简单,移除成功的话,需要释放资源。
其他方法
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
如果缓存踢出对象需要显性释放资源的话,需要重写这个方法。
evicted为true的话,说明是由于空间不足造成的;为false的话,说明是由于put或者remove造成的。
protected V create(K key) {
return null;
}
如果缓存miss的话,需要根据对应的key产生value的话,需要重写此方法。
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
返回某个元素的大小,在下面方法的基础上,添加了不为负数的判断。
protected int sizeOf(K key, V value) {
return 1;
}
返回某个元素的大小,默认返回的元素大小为元素的数目1,一般需要重写。
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
清空缓存队列。
到此为止。