博主最近在學習Bitmap高效加載和3級緩存(內存緩存、本地緩存和網絡緩存)管理。LruCache(least recent used cache)是一種高效且普遍使用的管理策略。因此,便開啓了LruCache源碼學習之旅。
注意,本文中涉及的LruCache源碼爲support v4包中的LruCache。
Table of Contents
1 LruCache結構
private final LinkedHashMap<K, V> 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()方法。
2 待開發者覆寫的方法
2.1 sizeOf
方法源碼爲:
protected int sizeOf(K key, V value) {
return 1;
}
該方法的意義在於:開發者可按單位自己定義緩存中每個對象的大小,若緩存中存放的是Bitmap,則可寫爲:
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount() / 1024;
}
那麼size對應的單位爲kb。
2.2 entryRemoved()回調
在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中。
2.3 create()方法
該方法的默認實現爲:
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。
3 公共方法——put()
看一下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<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);
}
}
進入無限循環,且進入synchronized代碼塊進行線程保護;
當size小於等於maxSize或map爲空時,將直接退出循環;
通過LinkedHashMap的迭代器獲取下一個緩存,next在隊列中是緩存時間最長的、最少用到的項目,獲得後將它刪除;這裏也體現了least recent used的思想。
相應地減少緩存的真實大小,並讓evictionCount數量+1,記錄被驅逐的條目的數量;
離開synchronized代碼塊;
調用entryRemoved()方法,注意參數的設置代表了不同的意義,該方法由開發者實現,完成刪除一個條目後的回調;可以看見entryRemoved()調用都是非線程安全的。
繼續循環,直到緩存大小滿足要求。
4 公共方法——get()
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的思想。
5 公共方法——remove()
源碼如下:
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回調。
6 LinkedHashMap中LRU思想的實現
在3中,LinkedHashMap.put()方法中,通過LinkedEntryIterator的next()中,直接返回了最老的對象,並將它刪除,實現了緩存的更新。
6.1 LinkedHashMap的數據結構
//The head (eldest) of the doubly linked list.
transient LinkedHashMapEntry<K,V> head;
// The tail (youngest) of the doubly linked list.
transient LinkedHashMapEntry<K,V> tail;
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
從註釋就可看出,head指向最老的對象,tail指向最新的對象;
LinkedHashMapEntry就是HashMap中的Node。
HashMap使用Node[] table通過數組+鏈表的形式存儲數據。Node中含有hash code、key和value。通過hash code可以找到Node鏈首在table[]中的位置。關於HashMap這裏就不做多的討論。
6.2 添加數據時,LRU思想的實現
LinkedHashMap.put()時直接使用HashMap的put() -> putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab;
Node<K,V> 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<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMapEntry<K,V> p = new LinkedHashMapEntry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
LinkedHashMapEntry<K,V> 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<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
removeEldestEntry()將通過size來判斷是否需要移除最老的數據;而最老的數據就在head。
綜上,完成了LinkedHashMap關於LRU的緩存更新。
6.3 Iterator.next()返回最老的數據
數組結構如下:
abstract class LinkedHashIterator {
LinkedHashMapEntry<K,V> next;
LinkedHashMapEntry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
next = head;
expectedModCount = modCount;
current = null;
}
//.......
}
nextNode()方法:
final LinkedHashMapEntry<K,V> nextNode() {
LinkedHashMapEntry<K,V> e = next;
//......
current = e;
next = e.after;
return e;
}
可以看到,初始化時,直接把head賦值給next;
nextNode中直接返回的是當前的next。
7 結語
最後,做一下小結:
要使用LruCache,需開發者結合自己的需求分別實現sizeOf、create()和entryRemoved()。其中sizeOf最好實現一下,以區分size和count的意義,其他兩個方法可以選擇實現。
create()和entryRemoved()在LruCache中調用是非線程安全的,這一點需要開發者注意。
get()、put()和remove()的核心都是線程安全的;
LruCache實際是以LinkedHashMap爲依託,進行存取和放置。由於LinkedHashMap派生自HashMap,故它可通過數組+鏈表的方式存儲數據。
而LinkedHashMap中又維持了head和tail組成的雙端鏈表,使其實現了LRU的思想。