在讲解源码之前,首先我们要知道什么是Lru?
LRU是Least Recently Used 近期最少使用算法。它的核心思想就是当缓存空间存满的时候,会优先淘汰那些近期最少使用的缓存对象。
我们来看看LruCache的结构:
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size; //当前缓存的大小
private int maxSize; //最大可缓存的大小
private int putCount; //put方法被调用的次数
private int createCount; //create()方法被调用的次数
private int evictionCount; //调整的次数
private int hitCount; //命中的次数
private int missCount; //没有命中的次数
我们可以看到,原来LruCache底层封装了LinkedHashMap。LinkedHashMap继承自HashMap,不同的是LinkedHashMap维护的是一个双向链表。
static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
LinkedEntry<K, V> nxt;
LinkedEntry<K, V> prv;
/** Create the header entry */
LinkedEntry() {
super(null, null, 0, null);
nxt = prv = this;
}
。。。。。。
}
当要添加一个新的节点的时候,首先就会调用put方法:
这里put方法调用的是HashMap里面的put方法,但是放心,它最终会调用LinkedHashMap的addNewEntry方法
@Override
public V put(K key, V value) {
.........
addNewEntry(key, value, hash, index);
return null;
}
@Override void addNewEntry(K key, V value, int hash, int index) {
LinkedEntry<K, V> header = this.header;
// Remove eldest entry if instructed to do so.
LinkedEntry<K, V> eldest = header.nxt;
if (eldest != header && removeEldestEntry(eldest)) {
remove(eldest.key);
}
// Create new entry, link it on to list, and put it into table
LinkedEntry<K, V> oldTail = header.prv;
LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
key, value, hash, table[index], header, oldTail);
table[index] = oldTail.nxt = header.prv = newTail;
}
当要获取的节点的时候,肯定就是要调用get方法
@Override public V get(Object key) {
/*
* This method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
return null;
}
基本的添加操作看代码基本可以看懂了,这里最重要的是makeTail()方法,那我们就来看看这个方法:
private void makeTail(LinkedEntry<K, V> e) {
// Unlink e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;
// Relink e as tail
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}
原来这个get操作就是将get到的节点放到队列的尾部,这样也符合Lru的思想。
这个首先分两步,第一个是先创建节点,接着是放到队尾。
注意:
- 列表不为空的时候,header.nxt指向第一个节点,header.pre指向尾节点
- 列表空的时候,header.nxt和header.pre都指向自己
- accessOrder是指明排序的方式,当为false的时候,是顺序插入,新加入的节点都是放到尾部,当为true的时候,若有访问或者更新节点的时候,该节点会放到尾部
讲完了LruCache的数据结构,我们正式讲解源码分析!
我们先看看LruCache的用法,我们以Bitmap为例:
private LruCache<String,Bitmap> mMemoryCache;
//实例化对象
mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
//计算每一个节点的占用内存大小
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
//内存缓存的添加
mMemoryCache.put(key,bitmap)
//内存缓存的获取
mMemoryCache.get(key);
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);
//若这个节点已经存在的话,就要减回刚才的添加的内存,因为假如存在的话,会返回一个value;若是新添加的节点,则会返回null
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
//默认是空方法,自己可以在构造方法那些自己覆写该方法,主要是处理更新节点之后的操作
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
添加节点的代码是进行了同步的,所以是线程安全的。这里最后调用了trimToSize(maxSize)方法。我们看看这个方法:
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 || map.isEmpty()) {
break;
}
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
这里是一个while(true)的死循环,当前缓存大小size小于规定的最大缓存的时候,就可以退出,否则就不断删除第一个节点,也是lru的核心所在。
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
get方法:
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++;
}
/*
* 自己构造新的值
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
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;
}
}
若当前的key不存在链表当中,就调用create(key)方法,这个方法默认是返回null的,但是自己也是可以定义的,在构造方法那些自己进行覆写。若是构造了新的节点不为空的话,就相当于还要添加缓存大小,计算有没有超过最大缓存空间。
最后总结:
LruCache底层封装的是LinkedHashMap,当有节点进行更新或者访问的时候,都会将该节点放到尾部,当要添加节点的时候,就还要判断缓存空间是否有超出,如果超出的话,就不断删除第一个节点,直到缓存空间小于规定的最大缓存空间。