LRU
算法,中文叫 近期最少使用
算法。这个算法的思想是当容量已经满的情况下,我们再次向容器里面存放东西的时候,我们需要先删除之前使用最少的元素,因为这个元素在新元素插入之间一使用的最少,我们认为在后面的过程中,这个元素依然是使用次数最少的,当容积不够的时候,我们当然要删除后面过程中使用次数最少的,腾出空间给新的元素。
Android 里面实现了这种算法,名字叫 LruCache
。下面我们从源码(android sdk 23)的角度来解析这个类
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map; // 最重要的一个变量,上面所说的原理就是利用这个 LinkedHashMap 实现的
/** Size of this cache in units. Not necessarily the number of elements. */
private int size; // 已经缓存的元素个数
private int maxSize; // 缓存大小
private int putCount; // 往里面添加元素计数器
private int createCount;// key 为 null 的情况下,默认生成对象的个数
private int evictionCount; // 当加入的元素超过 maxSize 后,调整大小的次数
private int hitCount; // 命中的次数
private int missCount; // 没有命中的次数
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
// 省略其他代码
}
我们先来看看 put
方法是如何工作的
public final V put(K key, V value) {
// 如果 key 或 value 为 null 那么抛出异常,说明不接受 null
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous; // 记录 key 的旧值
synchronized (this) {
putCount++; // 插入计数器计数
size += safeSizeOf(key, value); // 修正 size
previous = map.put(key, value); // 取出 LinkedHashMap 里面的旧值
if (previous != null) { // 之前的确保存过
size -= safeSizeOf(key, previous); // 修正 size
}
}
if (previous != null) { // 如果之前保存过,那么删掉这个节点,但是看源码发现,entryRemoved 是空
entryRemoved(false, key, previous, value);
}
// 调整大小,也是最重要的方法之一
trimToSize(maxSize);
// 返回之前插入的值
return previous;
}
刚才提到了 trimToSize()
,那我们来看看这个方法的具体实现。
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!");
}
// size(缓存的元素个数) 小于 maxSize(容量) 那么跳出循环
if (size <= maxSize) {
break;
}
// eldest() 取 LinkedHashMap 里面最年老的元素
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) { // 为空说明 LinkedHashMap 里面没有元素了
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
// 删掉 最近最少使用的
map.remove(key);
size -= safeSizeOf(key, value); // 修正 size
evictionCount++; // 缩减计数器
}
// 删除元素,entryRemoved 是一个空方法
entryRemoved(true, key, value, null);
}
}
trimToSize()
方法里面 LinkedHashMap.eldest()
方法很重要,LinkedHashMap.eldest()
的作用就是取出最年老,即最近最少使用的元素。如何取到最近最少使用的元素就是 LinkedHashMap 里面的东西了。我们接下来看看 LinkedHashMap 的源码
public class LinkedHashMap<K, V> extends HashMap<K, V> {
/**
* A dummy entry in the circular linked list of entries in the map.
* The first real entry is header.nxt, and the last is header.prv.
* If the map is empty, header.nxt == header && header.prv == header.
*/
transient LinkedEntry<K, V> header;
/**
* True if access ordered, false if insertion ordered.
*/
private final boolean accessOrder;
/**
* LinkedEntry adds nxt/prv double-links to plain HashMapEntry.
*/
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;
}
/** Create a normal entry */
LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
super(key, value, hash, next);
this.nxt = nxt;
this.prv = prv;
}
}
// 省略其他代码
}
首先,有一个 LinkedEntry 内部类,继承自 HashMapEntry , 里面有 2 个引用分别指向之前和之后的元素,说明这个是一个双向链表结构。 LinkedEntry<K, V> header
这个变量就是双向链表的表头了。 accessOrder
这个的意思是是否需要排序,这个在后面会用到。我们先看看 eldest()
方法。
public Entry<K, V> eldest() {
LinkedEntry<K, V> eldest = header.nxt;
return eldest != header ? eldest : null;
}
方法很简单,就是取出 header 的 nxt 元素。那为什么 header 的 nxt 袁术就是最近最少使用的元素呢?这个是因为在 put
和 get
过程中,如果 accessOrder
设置为 true 的话,会把对应 key 的 LinkedEntry 节点放到队列的尾部。下面我们通过 get
源码分析下这个过程。
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.
*/
// 如果 key 为空,做key 为空的特殊处理
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
// 计算 hash 值
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
// 遍历,找到 key 对应的元素节点
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) // accessOrder 如果为 true ,那么就执行将元素节点放到队列尾部的操作
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
// 如果这个 key 之前没有 put 过,那么就返回 null.
return null;
}
看代码发现, accessOrder
为 true
的时候,我们才会去做将元素节点放到队列的操作,而在 LruCache 里面,LinkedHashMap 的这个值是 true 的。整个的 LruCache 分析就到这里为止。
最后我们看下 makeTail 的源码
private void makeTail(LinkedEntry<K, V> e) {
// Unlink e 断开 e 的链接
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;
// Relink e as tail 将 e 链接到队列尾部。所以队列头部就是最早put进去但是一直没使用的
// 也就是所谓的 "最近最少使用"
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}