LruCache原理分析

LruCache这个类在我们现在应用的开发中已经被普遍使用了,今天我们就深度解析这个类,从原理上掌握作者的设计思想以及实现原理

什么是LruCache

借官方的描述,LruCache就是一个持有一定数量强引用数据的缓存。当访问一个数据时,这个数据就会被移动到数据队列的头部(经常用到的数据),当数据添加到缓存满时,队列尾部的数据(也就是不常用到的数据)会被删除并被回收,这就是LruCache的定义,同时这其中也包含了它的实现思想,采用了LRU算法用来管理数据。

LruCache能做什么

在我们平时的开发中,这个用的最多的就是缓存内存图片了,这里我就不在啰嗦,具体实例官方文档已经很详细了,我在这里要说的是,这个不一定用来缓存Bitmap,程序设计中凡是涉及LRU思想的逻辑的实现,都可以使用LruCache。

LruCache原理解析

下面就让我们从源码的角度去解析LruCache的实现

LruCache的成员变量

1
2
3
4
5
6
7
8
9
10
11
private final LinkedHashMap<K, V> map; //LruCache中的数据存储结构是LinkedHashMap

/** Size of this cache in units. Not necessarily the number of elements. */
private int size; //参考注释,这个只是个单位数,并不是指Map中数据的个数,比如我们缓存Bitmap时,可以定义成Bitmap的大小,单位可以是KB,MB等;但是我们也可以按数量来缓存,比如一个Bitmap,就是1等
private int maxSize; //缓存的最大单位数,这个表示数据的单位要和size统一

private int putCount; //添加数据的次数
private int createCount; //创建数据的次数
private int evictionCount; //删除数据的次数
private int hitCount; //从缓存中找到数据的次数
private int missCount; //从缓存中没有找到数据的次数

构造方法

如下,LruCache构造方法中最关键的就是对LinkedHashMap的初始化,第三个参数设置为true;这个参数的作用就是调用map的get方法时,如果找到数据,是否将找到的数据移动到双向链表的尾部

1
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);

基本的entryRemoved方法

下面来分析一下entryRemoved方法,这个方法是个空方法,我们先看看方法的参数:

  • evicted 这个参数表示是否因为空间不足发生移除不常使用的数据;true表示是因为空间不足移除数据,false则相反
  • key 移除数据的key值
  • oldValue 正真从链表中移除的数据
  • newValue key键对应的值被替换的新数据

这个方法是个空方法,我们很少用到它,它的作用是什么呢?
这个方法的主要作用是帮助我们对移除的数据进行一些回收操作的,比如Bitmap,我们知道在Android2.3.3之前,Bitmap的像素数据是放在native的堆中的,gc是不能对其进行回收的,这时候我们需要主动调用Bitmap的recycle方法来释放位图的像素内存(Android3.0之后位图的像素数据被关联在jvm的堆内存中,这时就不需要调用recycle方法了),那么entryRemoved在这个时候就会用到,如果LruCache存放的一个位图因为内存不足时被remove,这时要想正真回收位图内存,我们还需主动回收

1
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

基本的sizeOf方法

这个方法就是根据单位来计算key对应value数据大小,比如存放的是Bitmap,sizeOf我们可以定义返回KB,MB等,不过这个只要保持和maxSize单位统一就可以了

1
2
3
protected int sizeOf(K key, V value) {
return 1;
}

trimToSize方法

这个方法是用来调整cache单位数据大小的,我们就从代码中去详细分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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) { //当cache数据大小小于最大数据限制时退出
break;
}

//如果cache数据超出限制,则取出cache中最不常用的数据,并将其从cache中移除
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}

key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value); //修改cache的size
evictionCount++;
}

entryRemoved(true, key, value, null); //这个方法解释了
}
}

常用的put方法

我们来分析下最常用的put方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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); //修改cache的size
previous = map.put(key, value); //将key对应的数据put到链表中
if (previous != null) {
size -= safeSizeOf(key, previous); //如果发生了相同key数据替换,则对齐cache的size
}
}

if (previous != null) {
entryRemoved(false, key, previous, value); //这个上边已经详细介绍过了
}

trimToSize(maxSize); //调整cache的size
return previous;
}

常用的get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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++;
}

/*
* 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.
*/

V createdValue = create(key); //如果没有在缓存中找到数据,那么调用调用create方法创建数据
if (createdValue == null) {
return null;
}

synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);

//如果key对应的数据被创建的数据替换则复位key对应的数据,没有则对齐cache的size
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}

if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}

create方法

在上述的get方法中我们看到了一个create方法,这个方法作用就是创建key对应的数据,但是使用这个方法的时候一定要慎重,因为这个方法可能是个耗时方法,比如Bitmap的decode

小结

至此,LruCache的关键原理已经讲述完毕,其他的方法比较简单,读者可以自行分析,希望我的文章能帮助到大家