一、写在前面
在学习Android的某个功能或者解析源码时,首先要去查找相关的资料,了解它的设计,然后根据设计去思考大神们如何用代码来表达出设计的思想,最后从实现的角度去分析源码,才能深入理解;
理解源码和实现时,不必每一行代码都搞懂,最开始学习的时候只需要注重整体的代码思路和逻辑即可,这样做可以平缓学习曲线,避免因为感觉太过困难而放弃学习;
在上一篇文章LruCache原理和源码分析(一)中,详细讲解了HashMap和LinkedHashMap的原理和源码;
如果还没有看过LruCache原理和源码分析(一),戳这里
接下来,详细分析LruCache的原理和使用~~~~
二、LruCache基本原理
LRU全称为Least Recently Used,即最近最少使用。
我们的缓存容量是有限的,它会面临一个问题:当有新的内容需要加入我们的缓存,但我们的缓存空闲的空间不足以放进新的内容时,如何舍弃原有的部分内容从而腾出空间用来放新的内容。解决这个问题的算法有多种,比如LRU,LFU,FIFO等。
LRU更具体的讲,淘汰最长时间未使用的对象,如果我们缓存对象的顺序是:B C B D A C A ,当需要淘汰一个对象时,根据LRU算法,则淘汰的是B,因为它是最长时间未被使用的
LruCache利用LinkedHashMap的一个特性( accessOrder=true时基于访问顺序 )再加上对LinkedHashMap的数据操作上锁实现的缓存策略
熟悉了LinkedHashMap之后,我们发现,通过它来实现Lru算法也就变得理所当然了。
我们所需要做的,就只剩下定义缓存的最大大小,记录缓存当前大小,在放入新数据时检查是否超过最大大小。
三、LruCache使用方法
3.1.使用的时候记住几点即可:
1.(必填)你需要提供一个缓存容量作为构造参数。
2.(必填)覆写 sizeOf 方法 ,自定义设计一条数据放进来的容量计算,如果不覆写就无法预知数据的容量,不能保证缓存容量限定在最大容量以内。
3.(选填) 覆写 entryRemoved 方法 ,你可以知道最少使用的缓存被清除时的数据( evicted, key, oldValue, newVaule )。
4.(记住)LruCache是线程安全的,在内部的 get、put、remove 包括 trimToSize 都是安全的(因为都上锁了)。
5.(选填) 还有就是覆写 create 方法。
一般做到 1、2就足够了,3、5一般用不到,4使用的时候记住就可以了。
以下是 一个 LruCache 实现 Bitmap 小缓存的案例, entryRemoved 里的自定义逻辑可以无视,想看的可以去到我的我的展示 demo 里的看自定义 entryRemoved 逻辑。
3.2.使用实例
/**
* maxMemory的单位是kb,那么sizeOf()方法返回的值的单位也必须是kb,两者的单位必须一致,记住这一点就可以了
*/
int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
int cacheSize=maxMemory/8;
private LruCache bitmapCache;
this.bitmapCache = new LruCache(maxMemory) {
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight/1024;
}
};
四、LruCache重要方法分析
4.1.put(key,value)方法
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;
}
主要逻辑是,计算新增加的大小,加入size,然后把key-value放入map中,如果是更新旧的数据(map.put(key, value)会返回之前的value),则减去旧数据的大小,并调用entryRemoved(false, key, previous, value)方法通知旧数据被更新为新的值,最后也是调用trimToSize(maxSize)修整缓存的大小。
剩下的其他方法,比如删除里面的对象,或进行调整大小的操作,逻辑上都和上面的类似,这里略过。
LruCache还定义了一些变量用于统计缓存命中率等,这里也不再进行赘述。
4.2.trimToSize(maxSize)方法
public void trimToSize(int maxSize) {
/*
* 这是一个死循环,
* 1.只有 扩容 的情况下能立即跳出
* 2.非扩容的情况下,map的数据会一个一个删除,直到map里没有值了,就会跳出
*/
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!");
}
// 如果是 扩容 或者 map为空了,就会中断跳出死循环,因为扩容不会涉及到丢弃数据的情况
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);
}
}
4.3.get(key)方法
/**
* 根据 key 查询缓存,如果存在于缓存或者被 create 方法创建了。
* 如果值返回了,那么它将被移动到双向循环链表的的尾部。
* 如果如果没有缓存的值,则返回 null。
*/
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
// 关键点:LinkedHashMap每次get都会基于访问顺序来重整数据顺序
mapValue = map.get(key);
if (mapValue != null) {// 当能获取到对应的值时,返回该值
// 计算 命中次数
hitCount++;
return mapValue;
}
// 计算 丢失次数
missCount++;
}
/*
* 正常情况走不到这里(不覆写create方法走不到下面 )
* 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
* 因为默认的 create(K key) 逻辑为null
* 尝试创建一个值,这个方法的默认实现是直接返回null。但是在它的设计中,这个方法可能执行完成之后map已* * 经有了变化。
*/
V createdValue = create(key);
if (createdValue == null) {//如果不为没有命名的key创建新值,则直接返回
return null;
}
synchronized (this) {
createCount++;
//将创建的值放入map中,如果map在前面的过程中正好放入了这对key-value,那么会返回放入的value
mapValue = map.put(key, createdValue);
if (mapValue != null) {//如果不为空,说明不需要我们所创建的值,所以又把返回的值放进去
/*
* 有冲突
* 所以 撤销 刚才的 操作
* 将 之前相同key 的值 重新放回去
*/
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);//为空,说明我们更新了这个key的值,需要重新计算大小
}
}
if (mapValue != null) {//上面放入的值有冲突
/*
* 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
* 告诉 自定义 的 entryRemoved 方法
*/
entryRemoved(false, key, createdValue, mapValue);// 通知之前创建的值已经被移除,而改为mapValue
return mapValue;
} else {
trimToSize(maxSize);//没有冲突时,因为放入了新创建的值,大小已经有变化,所以需要修整大小
return createdValue;
}
}
LruCache是可能被多个线程同时访问的,所以在读写map时进行加锁。当获取不到对应的key的值时,它会调用其create(K key)方法,这个方法用于当缓存没有命名时计算一个key所对应的值,它的默认实现是直接返回null。这个方法并没有加上同步锁,也就是在它进行创建时,map可能已经有了变化。
所以在get方法中,如果create(key)返回不为null,会再把它给放到map中,并检查是否在它创建的期间已经有其他对象也进行创建并放到map中了,如果有,则会放弃这个创建的对象,而把之前的对象留下,否则因为我们放入了新创建的值,所以要计算现在的大小并进行trimToSize。
五、总结
- LruCache 是通过 LinkedHashMap 构造方法的第三个参数的 accessOrder=true 实现了 LinkedHashMap 的数据排序基于访问顺序(最近访问的数据会在链表尾部),在容量溢出的时候,将链表头部的数据移除。从而,实现了 LRU数据缓存机制;
- LruCache 在内部的get、put、remove包括 trimToSize 都是线程安全的;
- LruCache 自身并没有释放内存,将 LinkedHashMap的数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存。
- maxSize 和 sizeOf(K key, V value) 方法的覆写息息相关,必须相同单位。( 比如 maxSize 是7MB,自定义的 sizeOf 计算每个数据大小的时候必须能算出与MB之间有联系的单位 )
- 覆写 entryRemoved 方法能知道 LruCache 数据移除是是否发生了冲突,也可以去手动释放资源。
references:
LruCache源码解析
Android源码解析——LruCache