博主最近在学习Bitmap高效加载和3级缓存(内存缓存、本地缓存和网络缓存)管理。LruCache(least recent used cache)是一种高效且普遍使用的管理策略。因此,便开启了LruCache源码学习之旅。
注意,本文中涉及的LruCache源码为support v4包中的LruCache。
Table of Contents
1 LruCache结构
2 待开发者覆写的方法
2.1 sizeOf
2.2 entryRemoved()回调
2.3 create()方法
3 公共方法——put()
4 公共方法——get()
5 公共方法——remove()
6 LinkedHashMap中LRU思想的实现
6.1 LinkedHashMap的数据结构
6.2 添加数据时,LRU思想的实现
6.3 Iterator.next()返回最老的数据
7 结语
private final LinkedHashMap map;
private int size;
private int maxSize;
private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;
map: 利用LinkedHashMap实现key-value存储,LinkedHashMap是双端链表,可按访问顺序或存储顺序来进行排序;链头是“最老”的对象,链尾是“最年轻”的对象;
size:缓存的真实大小,该大小指的是带有单位的大小,如byte或kb等;
maxSize:缓存真实大小的最大值;
putCount:用于记录put()方法被调用的次数;
createCount:用于记录create()方法被调用的次数;
evictionCount:用于记录缓存中被驱逐项目的数量;
hitCount:调用get(key)方法时,若缓存中存在key对应的value,即命中,该变量记录了缓存命中的次数;
missCount:调用get(key)方法时,若缓存中存在key对应的value,即未命中,该变量记录了缓存未命中的次数;
上述变量均对应相关公开的get()方法。
方法源码为:
protected int sizeOf(K key, V value) {
return 1;
}
该方法的意义在于:开发者可按单位自己定义缓存中每个对象的大小,若缓存中存放的是Bitmap,则可写为:
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount() / 1024;
}
那么size对应的单位为kb。
在LruCache中,该方法是个空方法,可由开发者自定义实现。
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
当调用了remove()、get()、put()或内部调用了trimToSize()的时候,会在不同情况下删除某个缓存,此时会回调entryRemoved;
根据各个参数,可以判断回调的具体时机;
若evicted为true,则回调发生在trimToSize()里,表示要删除一些缓存来控制缓存池的大小;这种情况下的删除就叫eviction,表示驱逐、赶出。
若evicted为false,则表示发生在remove、get或put内部。
若newValue不是null,那么回调肯定发生在put()当中;否则发生在trimToSize或remove中。
该方法的默认实现为:
protected V create(K key) {
return null;
}
若不覆写,则表示缓存池针对key不会创建任何对象;
该方法只会在get()中调用。
当用某个key来get(V key)一个值,但get不到对象时,会调用该方法,根据开发者的意图缓存池是否需要自己来创建这个key对应的value。
调用该方法时,是线程不安全的;针对多线程,缓存池会选择舍弃刚创建的对象;
什么叫舍弃?看一下这种情况:在多线程的环境下,线程A在缓存池中get(key)得不到值,于是调用create(key)来创建这个值;若同时,线程B向缓存池put了该key对应的值;此时缓存池会舍弃掉线程A正在创建的对象,采纳线程B已经put进入的value。
看一下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++;
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;
}
看一下put()方法的逻辑:
首先进入synchronized语块,线程安全;
putCount++,记录调用put()的次数;
size增加,记录缓存的实际大小;
将缓存的value放进map里,并获得可能的旧值,若存在旧值,那么记录缓存实际大小的size将调整。
若存在旧值,将调用entryRemoved()方法,该方法由开发者实现,作为替换了缓存值后的回调,可以看到,entryRemoved()方法已经不是线程安全的了。
调用trimToSize,比较size和maxSize,看是否需要通过删除一些缓存来保持缓存池的大小。
返回替换的旧值。
其中trimToSize()方法源码如下:
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
//......
//......
if (size <= maxSize || map.isEmpty()) break;
Map.Entry toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
进入无限循环,且进入synchronized代码块进行线程保护;
当size小于等于maxSize或map为空时,将直接退出循环;
通过LinkedHashMap的迭代器获取下一个缓存,next在队列中是缓存时间最长的、最少用到的项目,获得后将它删除;这里也体现了least recent used的思想。
相应地减少缓存的真实大小,并让evictionCount数量+1,记录被驱逐的条目的数量;
离开synchronized代码块;
调用entryRemoved()方法,注意参数的设置代表了不同的意义,该方法由开发者实现,完成删除一个条目后的回调;可以看见entryRemoved()调用都是非线程安全的。
继续循环,直到缓存大小满足要求。
get()方法的源码如下:
public final V get(K key) {
//......
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
首先,进入synchronized语块,线程安全;
从map中获取值,若该值存在,hitCount +1,并返回取到的值。
若此时map中不存在该值,missCount +1。
离开synchronized语块,线程不安全;
若未命中,调用create()方法,由缓存池创建该key对应的value;若缓存池不创建该value,则直接返回null;
若缓存池创建了该value,则再次进入synchronized语块,线程安全;
此时createCount + 1;
map put()创建的新值;
若在create()过程中,其他线程针对该key,put()了某个value至缓存池,此时put()将返回一个非null值。那么此时,将会把该值重新put()进map,保留这个值,并调用entryRemoved()舍弃刚才create()创建的的新值。
若在create()过程中,其他线程没有put()入value,则根据新创建的值,调整缓存池的大小。
注意,使用LinkedHashMap的get()方法时,返回的值将被重新插入到队列的最前端。这里也体现了least recent used的思想。
源码如下:
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 -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
从map中删除指定key的value;
若成功删除,要调整缓存池的大小,以及调用entryRemoved回调。
在3中,LinkedHashMap.put()方法中,通过LinkedEntryIterator的next()中,直接返回了最老的对象,并将它删除,实现了缓存的更新。
//The head (eldest) of the doubly linked list.
transient LinkedHashMapEntry head;
// The tail (youngest) of the doubly linked list.
transient LinkedHashMapEntry tail;
static class LinkedHashMapEntry extends HashMap.Node {
LinkedHashMapEntry before, after;
LinkedHashMapEntry(int hash, K key, V value, Node next) {
super(hash, key, value, next);
}
}
从注释就可看出,head指向最老的对象,tail指向最新的对象;
LinkedHashMapEntry就是HashMap中的Node。
HashMap使用Node[] table通过数组+链表的形式存储数据。Node中含有hash code、key和value。通过hash code可以找到Node链首在table[]中的位置。关于HashMap这里就不做多的讨论。
LinkedHashMap.put()时直接使用HashMap的put() -> putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node[] tab;
Node p;
int n, i;
//table为null时初始化table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//hash在table中没有数据,直接加入node
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//......
//之后的流程不贴代码,直接以注释说明
//hash码在table中有数据时,需要比较hash码和key
//若相等,则替换已有值
//若不等,则在该table[index]下的Node链中,队尾加入新的Node
afterNodeInsertion(evict);
return null;
}
其中LinkedHashMap自己实现了newNode()和afterNodeInsertion。
newNode()源码如下:
Node newNode(int hash, K key, V value, Node e) {
LinkedHashMapEntry p = new LinkedHashMapEntry(hash, key, value, e);
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMapEntry p) {
LinkedHashMapEntry last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
当LinkedHashMap里还没有数据时,head和tail都为null,此时新加入的数据将赋给tail和head,二者此时指向同一个对象;
当LinkedHashMap里有数据后,旧的tail让after指向新数据,新数据成为tail。
所以,这个过程就遵循 了LRU的思想,最老的数据在head,最新的数据在tail。
afterNodeInsertion()是在添加了数据之后调用,源码如下:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMapEntry first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
removeEldestEntry()将通过size来判断是否需要移除最老的数据;而最老的数据就在head。
综上,完成了LinkedHashMap关于LRU的缓存更新。
数组结构如下:
abstract class LinkedHashIterator {
LinkedHashMapEntry next;
LinkedHashMapEntry current;
int expectedModCount;
LinkedHashIterator() {
next = head;
expectedModCount = modCount;
current = null;
}
//.......
}
nextNode()方法:
final LinkedHashMapEntry nextNode() {
LinkedHashMapEntry e = next;
//......
current = e;
next = e.after;
return e;
}
可以看到,初始化时,直接把head赋值给next;
nextNode中直接返回的是当前的next。
最后,做一下小结:
要使用LruCache,需开发者结合自己的需求分别实现sizeOf、create()和entryRemoved()。其中sizeOf最好实现一下,以区分size和count的意义,其他两个方法可以选择实现。
create()和entryRemoved()在LruCache中调用是非线程安全的,这一点需要开发者注意。
get()、put()和remove()的核心都是线程安全的;
LruCache实际是以LinkedHashMap为依托,进行存取和放置。由于LinkedHashMap派生自HashMap,故它可通过数组+链表的方式存储数据。
而LinkedHashMap中又维持了head和tail组成的双端链表,使其实现了LRU的思想。