LRUCache概述
在开发android应用时,加载图片我们都会加上二级缓存,一级是内存缓存,二级是硬盘缓存。在内存缓存中我们一般采用SDK提供给我们的LRUCahce。
LRU(least recently uses最近最少使用)是一种页面置换算法,最近最少使用的被替换掉。LRU算法是以时间轴为依据进行替换,而不是使用频率为依据替换。
常用的页面置换算法还有:
LFU(least frequently used 最不经常使用)是以单位时间内使用频率为依据进行替换,使用频率低的被替换掉。
FIFO(first in fist out 先进先出)按顺序进行替换。
LRUCache实现原理
先看一下LruCache的构造方法
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);
}
可以看到LruCache内部维护了一个LinkedHashMap,LinkedHashMap继承自HashMap,所以我们先来研究下HashMap的实现原理。
HashMap的实现原理
还是先看下构造方法
public HashMap(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("Capacity: " + capacity);
}
if (capacity == 0) {
@SuppressWarnings("unchecked")
HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
table = tab;
threshold = -1; // Forces first put() to replace EMPTY_TABLE
return;
}
if (capacity < MINIMUM_CAPACITY) {
capacity = MINIMUM_CAPACITY;
} else if (capacity > MAXIMUM_CAPACITY) {
capacity = MAXIMUM_CAPACITY;
} else {
capacity = Collections.roundUpToPowerOfTwo(capacity);
}
makeTable(capacity);
}
其中 Collections.roundUpToPowerOfTwo(capacity);
方法是得到一个不小于所给数的2^n的数,就是说假如传入参数10,那么返回不小于10的2的n次方数就是16。具体的可以看这里
看下makeTable
方法
private HashMapEntry<K, V>[] makeTable(int newCapacity) {
@SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable
= (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
table = newTable;
threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
return newTable;
}
可以看到,HashMap
的构造方法中生成了一个大小为2^n的HashMapEntry
的数组。我们看下HashMapEntry
的结构
static class HashMapEntry<K, V> implements Entry<K, V> {
final K key;
V value;
final int hash;
HashMapEntry<K, V> next;
其中有map
中的key、value
还有根据key
得到的hash
值和它的后继引用next
。其中key value
都是Object
。
既然是基于Map
的,那么我们看下HashMap
的put
方法和get
方法:
@Override public V put(K key, V value) {
if (key == null) {
//如果key为null调用该方法处理,需要注意的是HashMap允许key和value为null
return putValueForNullKey(value);
}
//根据key得到hash值
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
//根据hash值和内部维护数组的长度得到索引
int index = hash & (tab.length - 1);
//如果index处的e不为null,通过循环不断遍历e的下一个元素
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
// 找到指定 key 与需要放入的 key 相等(hash 值相同
// 并且key的值相等)说明map中已经存在该数据,那么执行替换
if (e.hash == hash && key.equals(e.key)) {
//该方法是空实现,注意LinkedHashMap中会用到
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// No entry for (non-null) key is present; create one
//index处的Entry为null,说明此处还没有数据Entry
modCount++;
if (size++ > threshold) {
//大于数组的长度,扩容 大小x2
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}
需要说明的是这句
int index = hash & (tab.length - 1);
首先HashMap
采用一种所谓的Hash 算法
来决定每个元素的存储位置。 Java中Object
提供一个hashCode()
方法,也就是说每个对象都有hashCode()
方法得到一个唯一的hash值
。
上面得到index的方法非常巧妙,它总是通过 h &(table.length -1) 来得到该对象的保存位置——而 HashMap 底层数组的长度总是 2 的 n 次方,当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。
也就是说该方法返回的值index
总是在0–table.length之间循环。也就是说虽然key的hash值不同,但是他们在HashMap
中的存储位置是一样的。你可能有点疑惑,我们马上就会找到答案。
那么put
方法会把key-value
放入HashMap
,首先根据key
的hash
值得到在HashMap
中的存储位置,如果该位置上有数据,并且key的值相等,那么直接执行修改value
的操作。如果不想的会执行addNewEntry(key, value, hash, index);
方法。这个方法是干什么的呢,我们来看下
void addNewEntry(K key, V value, int hash, int index) {
table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}
代码很简单,生成一个新的Entry
,并且把key、value、hash
相应赋值,然后最重要的是把HashMap
中index
位置的Entry
的引用付给了新Entry
的next
变量。也就是说在这里形成了一个Entry链,并且新的Entry
位于Entry
链的头部,然后把index
的位置上的值更改成了新生成的Entry
。 HashMap
的get
方法跟put
方法的逻辑基本是一致的,感兴趣的读者可以自己查阅。
那么分析到这里我们也就理解了HashMap
的基本结构,用一张图表示
可以看到HashMap
中维护了一个数组,并且先用数组进行定位查找,然后用链表维护数组中同一位置上的Entry
。所以说HashMap
效率高不是没有道理的。
LinkedHashMap的实现原理
同样的先看下构造方法:
public LinkedHashMap(
int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
header = new LinkedEntry<K, V>();
this.accessOrder = accessOrder;
}
在LruCache中是这样使用的: this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
注意accessOrder
的值为true,
首先调用父类也就是HashMap
的构造方法生成一个数组,然后new了一个LinkedEntry
做为header
,看下LinkedEntry
结构
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;
}
}
可以看到它继承自HashMap
中的HashMapEntry
,那么其中的数据就有: key、value、hash、next、nxt、prv。
可以看到LinkedHashMap
在HashMap
的基础上又增加了双向链表。也就是说LinkedHashMap的结构是数组+单向链表+双向链表。
那么这个双向链表是做什么用的呢。我们继续分析。
还记得HashMap
中的preModify
方法吗,LinkedHashMap
实现了该方法用于put
时替换index
位置已经存在并且key
值相等的节点:
@Override void preModify(HashMapEntry<K, V> e) {
if (accessOrder) {
// accessOrder为true
makeTail((LinkedEntry<K, V>) e);
}
}
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++;
}
可以看到首先把e从双向链表中移除,然后放到链表的最尾端。可以看到其中的链表不仅是双向的而且是循环的。联系到HashMap
中的put
中的的操作,这一步只是修改value
值并且更改在双向循环链表中的位置。
再看下LinkedHashMap
中的addNewEntry
方法:
@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;
}
做的事情是生成一个新的LinkedEntry
,把它的引用放到双向循环列表的尾部,并且把它的引用赋值到数组中相应位置上的值。
再看下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;
}
逻辑跟HashMap
基本一样,根据index
在数组中定位,然后在单向列表中查找。也就是不管我们在LinkedHashMap
中执行get
方法或者put
方法,最后操作的那个entry
都是插到双向链表的尾部。那么头部的nxt
的那个entry
也就是最久未使用的entry
依据这个特性我们就可以实现LRU
实现缓存。
依据看一张图
然而分析道这里实际上并没有用到双向循环列表,那么这个双向循环列表到底是干什么用的呢?
我们看下LruCache中的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;
}
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);
}
}
最后如果当前大小大于我们设定的最大缓存大小那么执行trimToSize
方法中根据LinkedHashMap
的entrySet
得到iterator
,然后执行next
方法,最后执行的是LinkedHashMap中的这些代码
LinkedEntry<K, V> next = header.nxt;
LinkedEntry<K, V> lastReturned = null;
int expectedModCount = modCount;
public final boolean hasNext() {
return next != header;
}
final LinkedEntry<K, V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
LinkedEntry<K, V> e = next;
if (e == header)
throw new NoSuchElementException();
next = e.nxt;
return lastReturned = e;
}
可以看到返回的是header.nxt,也就是双向循环链表中的第一个entry,那么也就是最久未使用的那个entry
。
总结
到此LruCache的基本原理我们都分析清楚了,并且梳理了HashMap和LinkedHashMap的内部实现原理,虽然我们在开发项目的时候对基本数据结构的内部原理不太关注,但是我们也应该学习其内部原理。毕竟基础才是最重要的。说到HashMap又想到了androidSDK中的SparseArray可以替代key为Integer类型的HashMap,感性却的可以看这篇文章