目的
在 Android 开发中,我们需要避免程序占用过多的内存资源或者存储空间,比如网络加载图片下载文件等,当缓存大小达到一定值的时候我们需要从缓存中释放空间存放新的缓存,LruCache 就是常用的用于管理缓存的类。
LruCache 缓存算法
Lru: Least Recently Used
原理: 当缓存大小大于最大值时,优先抛弃缓存中最近最少使用的缓存,直到当前缓存大小小于等于最大值。
LruCache LRU 实现
利用 LinkedHashMap 可以根据元素调用排序的特点,提供 get() put() remove() 等方法来操作缓存。
源码解析
构造方法:
// LruCache.java
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}
构造方法的核心就是创建了一个初始容量为 0 ,默认 loadFactor = 0.75f,根据元素访问排序的 LinkedHashMap.
// LruCache.java
public class LruCache {
private final LinkedHashMap map;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size; // 当前缓存大小,不一定跟 map 中的键值对数相同
private int maxSize; // 最大缓存大小
private int putCount; // 标记 put() 被调用次数
private int createCount; // 标记调用 create() 且返回不为 null 的次数
private int evictionCount; // 标记 map 中被抛弃的值的数量
private int hitCount; // 标记调用 get() 时存在值的次数
private int missCount; // 标记调用 get() 时不存在值的次数
...
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;
}
}
核心方法:
1.get()
// LruCache.java
public final V get(K key) {
if (key == null) { // 非空判断
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
// 从缓存 map 中获取 mapValue
mapValue = map.get(key);
// 非空判断
if (mapValue != null) {
// 不为空,累加 hitCount 并返回 mapValue
hitCount++;
return mapValue;
}
// mapValue 为空,累加 missCount
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*
* 创建一个 value。这里需要消耗点时间,map 有可能在 create() 结束的时候
* 发生变化。并发情况下,如果在执行 create() 的时候有别的 value 添加到 map 中
* 的时候,会保留别的 value 在 map 中并且丢弃 createdValue.
*/
V createdValue = create(key);
// 默认情况下返回 null
if (createdValue == null) {
return null;
}
// 同步锁代码块
synchronized (this) {
// 累加 createCount
createCount++;
// 把 createdValue 放进 map
// LinkedHashMap.put() 返回的是该 key 原本的 value (原本不存在时返回null)
// 无论 value 是否为 null createdValue 依然会添加到 map 中
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// 不为空则有别的 value 已经添加在 map 中,且重新把这个 value 放进 map 中
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
// 添加成功后根据 createdValue 的大小添加到 size
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) { // 已经存在别的 value
// 回调 entryRemoved()
entryRemoved(false, key, createdValue, mapValue);
// 返回原本存在的 value
return mapValue;
} else { // 添加成功
// 缩短缓存大小直到小于最大缓存值
trimToSize(maxSize);
// 返回创建的 createdValue
return createdValue;
}
}
protected V create(K key) {
return null;
}
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;
}
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
直接根据 key 从缓存 map 中直接获取 value,并进行非空判断:
- 非null : 直接返回 value,结束 get() 。
- null : 调用 create() 创建一个值若为 null 直接返回 null 结束 get(),不为 null 就调用 map.put() 到缓存中,由于存在并发的情况,所以有可能在 create() 执行过程中已经有别的 value 对应同一个 key 添加到 map 中的情况,所以根据判断 put() 返回值是否为空可以判断 map 中是否已经存在 key 对应的值 :
- null: 创建的 createValue 添加成功,调用 safeSizeOf() 判断 createValue 的大小并累加到
size
上,然后调用 trimTosize() 使缓存大小小于maxSize
,最后返回 createValue。 - 非null: map 中已存在别的值 mapValue ,重新把 mapValue 放进 map 中,然后调用回调方法 entryRemoved(),最后返回 mapValue。
- null: 创建的 createValue 添加成功,调用 safeSizeOf() 判断 createValue 的大小并累加到
在 get() 中有几个 LruCache 提供给我们根据业务需求重写的方法:
1.create()
默认直接返回 null,根据业务需求我们可以在发现 key 对应的值不存在的时候创建一个值放进缓存中。
2.sizeOf()
默认值为1,根据需求我们可以设置每个值的缓存大小,比如图片我们可以返回图片占用的内存大小。
3.entryRemoved()
默认空实现,每当有 value 从 map 中被移除或者被抛弃的时候会调用。
2.put()
// LruCache.java
public final V put(K key, V value) {
if (key == null || value == null) { // 非空判断
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) { // 同步锁代码块
// 累加 put() 调用次数
putCount++;
// 累加缓存大小
size += safeSizeOf(key, value);
// 添加到 map 中
previous = map.put(key, value);
// 如果该 key 对应的 value 之前已存在,
// 当前缓存大小就需要减掉之前的 value 大小
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
// 如果该 key 对应的 value 之前已存在,
// 调用回调方法 entryRemoved()
if (previous != null) {
entryRemoved(false, key, previous, value);
}
// 最后调整缓存大小
trimToSize(maxSize);
// 返回之前的 value 值,若 value 之前不存在则为 null
return previous;
}
放进缓存,并返回调用 put() 之前缓存中该 key 对应的 value。
3.trimToSize()
调整缓存大小直到小于设置的最大值为止。
// LruCache.java
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 最后一个键值对,即按照调用顺序末尾的键值对
Map.Entry toEvict = map.eldest();
if (toEvict == null) { // 若末尾值为 null,跳出循环方法结束
break;
}
// 获取 key
key = toEvict.getKey();
// 获取 value
value = toEvict.getValue();
// 从 map 中移除
map.remove(key);
// 从缓存大小中减去该值的大小
size -= safeSizeOf(key, value);
// 累加 evictionCount
evictionCount++;
}
// 调用回调方法 entryRemoved
entryRemoved(true, key, value, null);
}
}
每次循环都移除调用顺序末尾的缓存,直到当前缓存大小小于设置的最大值。
4.remove()
根据 Key 从 map 中移除缓存。
// LruCache.java
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) {
// 若移除之前值不为 null,当前缓存大小需要减去 previous 大小
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
// 调用回调方法 entryRemoved
entryRemoved(false, key, previous, null);
}
// 返回移除前的值,若移除前值不存在则返回 null
return previous;
}
其他方法:
1.resize()
重置 LruCache 的最大缓存大小。
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
// 重新赋值 maxSize 后要调整令 size <= maxSize
trimToSize(maxSize);
}
2.snapshot()
获取一个复制当前缓存 map 的 LinkedHashMap。
public synchronized final Map snapshot() {
return new LinkedHashMap(map);
}
3.evictAll()
清除所有缓存。
public final void evictAll() {
// 直接调用 trimToSize(-1) 则最后缓存大小会变成 0
trimToSize(-1); // -1 will evict 0-sized elements
}