https://www.jianshu.com/u/6a180f1b9934
-
比如你在玩一款游戏,游戏里有一个仓库,有20个位置可以存放装备。 -
1-20件装备都可以正常存放,并根据时间顺序进行了排序,最晚存放的装备排在最后。 -
当你使用其中任意一件装备的时候,这件装备的排序就变成了最后。 -
有一天你又获得了一件装备,但是你的仓库已经满了,只能扔一件才能存储刚刚获得的,扔的顺序是从头开始扔。新装备存储后,排序为最后。依次类推。
接下来我们通过正常的使用来分析源码的逻辑,以下所有源码均来自API 28
private void lruTest() {
LruCache<Integer, Integer> lruCache = new LruCache<>(5);
lruCache.put(1, 1);
lruCache.put(2, 2);
lruCache.put(3, 3);
lruCache.put(4, 4);
lruCache.put(5, 5);
for (Map.Entry<Integer, Integer> entry : lruCache.snapshot().entrySet()) {
Log.e("LRU", entry.getKey() + ":" + entry.getValue());
}
Log.e("LRU", "超出设定存储容量后");
lruCache.put(6, 6);
for (Map.Entry<Integer, Integer> entry : lruCache.snapshot().entrySet()) {
Log.e("LRU", entry.getKey() + ":" + entry.getValue());
}
}
---日志输出---
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 1:1
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 2:2
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 3:3
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 4:4
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 5:5
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 超出设定存储容量后
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 2:2
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 3:3
2019-11-18 18:21:03.624 24293-24293/com.we.we E/LRU: 4:4
2019-11-18 18:21:03.625 24293-24293/com.we.we E/LRU: 5:5
2019-11-18 18:21:03.625 24293-24293/com.we.we E/LRU: 6:6
LruCache构造函数
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
//初始化一个负载因子为0.75,启动访问顺序排序的LinkedHashMap对象。至于LinkedHashMap是什么东西,我们后续分析。
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
put()方法源码解析
/**
* 对于 key,缓存其相应的 value,key-value 条目放置于队尾
*
* @return 返回先前 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 +1, safeSizeOf()方法返回值默认是1
size += safeSizeOf(key, value);
//map.put()方法返被替换的值,如果没有被替换,则返回null。(只有当前key在put之前就已经有数据,这种情况存储才会被替换)
previous = map.put(key, value);
if (previous != null) {
//如果是被顶替的数据,那size需要-1,因为没有新增数据。
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
//当多线程中一个线程调用到了create方法,另一个线程调用到了put,此时一个key可能生成了两个值,因此需要通过重写这个方法进行处理。该方法默认为空方法。
entryRemoved(false, key, previous, value);
}
//判断是否超出设定容量,如果超出,则循环删除最早数据,直到容量<=设定值。
trimToSize(maxSize);
return previous;
}
-
首先会进行K/V的null判断,如果为null则抛出异常。 -
加锁并自增元素条目计数器,通过LinkedHashMap.put方法将K/V添加到LinkedHashMap对象中,如果put方法返回值不为null,则说明原来该key下有数据,因此只是更新key对应的数据并没有新增数据,所以此时size需 -1。 -
如果previous!=null,说明当前的put操作替换了之前的一个值,如果重写了entryRemoved()方法,则会回调到该方法中进行处理,如果没重写entryRemoved(),该方法默认不做任何处理。 -
调用trimToSize(maxSize)进行容量检测和处理。
trimToSize()方法源码解析,以及bug发现
/**
*删除最旧的元素,直到剩余数据总数 <= maxSize。
*/
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!");
}
//如果当前元素数<=设定的最大值,则直接break退出循环。
if (size <= maxSize) {
break;
}
// BEGIN LAYOUTLIB CHANGE
// get the last item in the linked list.
// This is not efficient, the goal here is to minimize the changes
// compared to the platform version.
Map.Entry<K, V> toEvict = null;
for (Map.Entry<K, V> entry : map.entrySet()) {
//获取最少使用的数据元素,此处的写法是源码的一个bug,后续我会详细说明。
toEvict = entry;
}
// END LAYOUTLIB CHANGE
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
//移除元素
map.remove(key);
//计数长度size -1;
size -= safeSizeOf(key, value);
//缓存移除的次数 +1
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
LruCache<Integer, Integer> lruCache = new LruCache<>(5);
lruCache.put(1, 1);
lruCache.put(2, 2);
lruCache.put(3, 3);
lruCache.put(4, 4);
lruCache.put(5, 5);
for (Map.Entry<Integer, Integer> entry : lruCache.snapshot().entrySet()) {
Log.e("LRU", entry.getKey() + ":" + entry.getValue());
}
2019-11-19 16:22:54.299 31747-31747/com.we.we E/LRU: 1:1
2019-11-19 16:22:54.299 31747-31747/com.we.we E/LRU: 2:2
2019-11-19 16:22:54.300 31747-31747/com.we.we E/LRU: 3:3
2019-11-19 16:22:54.300 31747-31747/com.we.we E/LRU: 4:4
2019-11-19 16:22:54.300 31747-31747/com.we.we E/LRU: 5:5
API 27
Map.Entry<K, V> toEvict = map.eldest();
//...
//eldest是被标记隐藏的. 实现如下:
public Entry<K, V> eldest() {
LinkedEntry<K, V> eldest = header.nxt;
return eldest != header ? eldest : null;
}
API 28
Map.Entry<K, V> toEvict = null;
for (Map.Entry<K, V> entry : map.entrySet()) {
toEvict = entry;
}
get()方法源码解析
/**
* 指定 key 对应的 value 值存在时返回,否则通过 create 方法创建相应的 key-value 对。
* 如果对应的 value 值被返回,那么这个 key-value 对将被移到队尾。
* 当返回 null 时,表明没有对应的 value 值并且也无法被创建
*/
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//通过LinkedHashMap对象获取当前key对应的value
mapValue = map.get(key);
if (mapValue != null) {
//缓存命中的次数+1
hitCount++;
//并return返回结束
return mapValue;
}
//缓存未命中的次数+1
missCount++;
}
/*
* 尝试创建一个value值,但是可能消耗很长时间,当create方法返回时,哈希表有几率发生变化
* 如果再哈希表中添加了一个有冲突的值,那么将保留原有的值,替换刚刚生成的value
*/
//create()是需要重写的方法,如果不重写默认返回null
V creaweValue = create(key);
if (creaweValue == null) {
return null;
}
synchronized (this) {
//创建 key 对应的 value 的次数+1
createCount++;
//map.put方法返回的是该key下原来的值,如果该key下原来没有值则返回null
mapValue = map.put(key, creaweValue);
if (mapValue != null) {
//mapValue不为null说明该key原来有对应的值,因此需要使用原来值替代刚刚生成的value。
map.put(key, mapValue);
} else {
//如果原来key没有对应的值,说明新增了一对键值对,因此需要size+1
size += safeSizeOf(key, creaweValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, creaweValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return creaweValue;
}
}
-
get()放中除了正常的判null以为,进行了加锁并通过LinkedHashMap.get(key)获取该key下的value。 -
如果LinkedHashMap.get(key)没有获取到value,则通过create()方法创建对应的value值,但create()默认返回null,需要重写进行创建处理。 -
如果重写了create()方法并且生成了value,则加锁通过LinkedHashMap.put()进行插入,并根据put方法的返回值判断该key之前是否有对应的value,如果有则重新将原有的值put替换刚刚生成的值。
或者当一个线程调用put而另一个线程调用了create方法为其创建值时这种情况下,在后续逻辑中会调用entryRemoved()方法进行处理。
remove()源码解析
/**
* 删除key对应的value
*
* 返回key对应的value值
*/
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 -1
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
-
其实LruCache类中方法数量不多,大部分都依赖于LinkedHashMap的方法,而LruCache只是在维护一个内部LinkedHashMap对象。
-
LruCache使用并不是线程安全的,因此提供了entryRemoved()方法用来重写解决异常情况。
-
LruCache的大部分方法中都进行了K/V判空,因此 LruCache 不支持空K和空V。