Android LruCache 源码解析
Lru算法,即最近最少使用算法。是对于内存管理的一种策略,会将最近最少使用的数据缓存移除。
举例说明:
若是缓存大小为4,按顺序添加数据分别是 1 2 3 4
此时访问数据 2 ,则访问完成后内部排序就会变成 1 3 4 2
若是再添加一个数据5,则会删除1,此时内部排序为3 4 2 5
即超过最大容量时,会将使用最少的时间最久的删除,然后添加新的数据
Android中的Lru算法
在android中,官方已经为我们实现了LruCache。其内部是通过LinkedHashMap来维护数据的。LinkedHashMap又是以HashMap为基础实现的有序的Map。
在Android中常用于图片的缓存。由于图片占的内存都比较大,而在android中内存又是非常重要并且紧缺的,因此不可能把所有图片都维持在内存中的。但是图片又通常是从网络加载的比较慢,要是每次使用都重新从网络加载,对于用户的体验不是太友好。
因此如何对图片进行合理的缓存就显得比较重要了,而Lru则是比较合理的一种缓存方式,将经常使用的缓存下来,而不经常使用则丢弃。
LruCache的使用
//在LruCache的注释中已经给出了示例
int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
LruCache构造函数需要传入一个maxSize作为缓存的大小。另外通常还需要重写sizeOf方法,该方法默认返回1。
缓存大小和缓存单位必须匹配。若是maxSize是以内存大小为单位的,则sizeOf方法必须返回每一个数据的实际大小。如上例,若是每个Bitmap的大小刚好为1MiB,则可以缓存4个Bitmap,若是不重写sizeOf的话,则会存储4 * 1024 * 1024个Bitmap
后面就是与Map的用法一致了,即通过put添加,通过get获取缓存实例。
接下来将具体分析一下LruCache的实现
LruCache中的变量
//内部维护的LinkedHashMap,通过键值对来存储数据。
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;
构造方法
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);
}
在构造函数中初始化LikedHashMap,这是管理缓存数据的主要成员。其内部通过数组+双向链表+红黑树(java 8)实现。前两个参数分别是哈希桶初始大小,负载因子。(具体请参看HashMap的源码)
第三个参数控制内部排列顺序:
false:按照插入顺序排列
true:按照最近使用次序排列
接下来就是最常用的方法get和put
get方法
该方法会返回缓存中的数据,并且将该数据移动到队尾(由LinkedHashMap实现)。
若是无法获得key对应的值,将会试图通过create方法创建一个value。该方法在LinkedHashMap中定义,并且默认是一个空实现,可根据需求重写该方法。若是create创建的值在插入map中时发现该值已经存在了(并发中可能会出现这种情况),则会丢弃创建值并不会进行插入。
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
// 若是在map中存在,则返回该值
V mapValue;
synchronized (this) {
mapValue = map.get(key);//该方法在LinkedHashMap中会将该数据移动到队尾
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
//试图通过create方法创建,默认空实现返回null
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
//创建成功则将结果存入缓存
//若是map.put放入的key在缓存中已经存在,则不替换该值,直接返回
//若是不存在,则正常插入并返回null
mapValue = map.put(key, createdValue);
if (mapValue != null) {
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
//从缓存中删除条目的时候调用,默认空实现。
//这里对应create创建成功但是插入失败的情况
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
//执行到这里则代表通过create创建的value满足条件,并且存入了缓存中
trimToSize(maxSize);//重新调整大小
return createdValue;
}
}
对于get方法中涉及的一些方法
//该方法在缓存未命中时调用,用来创建相应的key对应的value值
//如果无法计算出来则返回null,否则返回计算value值
//若是计算出来值后,对应的key已经在缓存中存在,该计算值将会被丢弃
//默认空实现返回null,可根据需求重写该方法。
protected V create(K key) {
return null;
}
//当数据从缓存中移除remove()或者替换put()的时候调用。
//第一个参数true表示自动的移除数据(数据超出缓存大小则会将最少使用的最久的数据删除),
//false表示被remove()移除或者被put()替换
//默认空实现,可根据需求进行重写。
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
//直接调用sizeOf方法
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
//调整缓存大小,该方法在后面的put方法后再进行解释
private void trimToSize(int maxSize){...}
说完了get方法,剩下的就是put了
put方法
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++;
//增加当前大小,并调用put插入数据
size += safeSizeOf(key, value);
previous = map.put(key, value);
//若是缓存中已经存在该key对应的value,则将当前大小减去被替换的value的大小
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
//替换的回调,上文已经说过
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//调整大小
trimToSize(maxSize);
return previous;
}
trimToSize
该方法在涉及到数据的添加的时候会被调用,用于计算当前大小是否已经超过了缓存大小
private void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
//若是当前size小于0,或者缓存为空但是当前大小不为空,则抛出异常
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 = null;
for (Map.Entry<K, V> entry : map.entrySet()) {
toEvict = entry;
}
if (toEvict == null) {
break;
}
/******************标记结束*****************/
key = toEvict.getKey();
value = toEvict.getValue();
//移除该元素
map.remove(key);
//计算当前大小并开始下一次循环,直至当前size不大于最大size
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
在android.util.LruCache(Android-28)中,实现代码如上,但是在android.support.v4.util.LruCache(28.0.0版本)中则有些不一样。
区别主要是在trimToSize方法中移除最久不使用的数据这里,上述代码已经标记出来了。
标记区的代码区别如下:
/*******android.support.v4.util.LruCache(28.0.0)**************/
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
/****************end *****************************************/
/************android.util.LruCache(android-28)****************/
Map.Entry<K, V> toEvict = null;
for (Map.Entry<K, V> entry : map.entrySet()) {
toEvict = entry;
}
if (toEvict == null) {
break;
}
/*******************end***************************************/
/*************android.util.LruCache(android-24~27)************/
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
/***********************end***********************************/
对于LinkedHashMap,内部是使用的HashMap实现的,不同的是LinkedHashMap对于HashMap的节点增加了两个元素before和after,用于将节点连接成双向链表。因此对于LinkedHashMap而言,可以看做是双链表。在Java8中,每次插入数据都是将数据插入到队尾。(具体可查看LinkedHashMap源码)
对比可以看到上面的标记区的代码的不同,在v4包中的LruCache通过map.entrySet().iterator().next()得到的是队首的节点,该节点在Java8中的确是最近最不常使用的节点。
在Android-24~27中的android.util包里的LruCache则是通过map.eldest()获得,该方法在Java8源码中是不存在的,是android添加的一个方法,该方法**public Map.Entry<K, V> eldest() { return head; }**返回的是head,也就是队首,与v4包获取到的节点是一样的。
在Android-28中的android.util包里的LruCache是通过遍历entrySet获取最后一个节点,也就是队尾节点。但是在Java8中队尾节点却是最近最新使用的数据,对于Lru删除这个节点的做法,让我很是疑惑。
remove方法
将对应Key移除缓存,若是成功则返回移除value,失败返回null
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
//移除map中的对应值,成功则返回移除的value
previous = map.remove(key);
if (previous != null) {
//成功则将当前size减去移除的大小
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
其余方法
//重新设置缓存大小
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
//一般需要自己实现,默认返回1
protected int sizeOf(K key, V value) {
return 1;
}
//移除缓存中所有数据
public final void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}
//获取当前已使用缓存大小
public synchronized final int size() {
return size;
}
//获取缓存总共大小
public synchronized final int maxSize() {
return maxSize;
}
public synchronized final int hitCount() {
return hitCount;
}
public synchronized final int missCount() {
return missCount;
}
public synchronized final int createCount() {
return createCount;
}
public synchronized final int putCount() {
return putCount;
}
public synchronized final int evictionCount() {
return evictionCount;
}
public synchronized final Map<K, V> snapshot() {
return new LinkedHashMap<K, V>(map);
}
@Override public synchronized final String toString() {
int accesses = hitCount + missCount;
int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
maxSize, hitCount, missCount, hitPercent);
}