深入源碼剖析LruCache,源碼剖析lrucache
引言:最近許多人在部落格中提到自己在面試時被問“LruCache 的原理是?”,發現自己之前完全沒有接觸過這個知識點,本著知其然知其所以然的態度,先搜尋了一些博文瞭解相關知識,就去看源碼了。現在大概知道 LruCache 是啥,寫個博文權當是學習筆記把
LruCache 的前世今生LruCache 是何方神聖?
我一般不喜歡野路子的定義,所以我摘選了 Android 官方對 LruCache 的定義:
A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection.
定義的意思是:LruCache 是對限定數量的緩衝對象持有強引用的緩衝,每一次緩衝對象被訪問,都會被移動到隊列的頭部。當有對象要被添加到已經達到數量上限的 LruCache 中,隊列尾部的對象將會被移除,而且可能會被記憶體回收機制回收。
所以從定義裡我們可以知道:LruCache 中有一個佇列儲存體對象的訪問順序,在 LruCache 達到儲存上線時,將會通過移除隊列中的隊尾元素為需要添加進來的元素騰出空間。
LruCache 中的隊列的存在意義是什嗎?
要探討這個問題首先我們要知道的是:LruCache 中的 Lru 指的是“Least Recently Used-近期最少使用演算法”。這就意味著,LruCache 應該是一個能夠判斷哪個緩衝對象是近期最少使用的緩衝類。解釋到這裡我相信大家都能明白為什麼需要引入隊列了。
引入隊列的意義在於,每一次 LruCache 中的某個緩衝對象被訪問,該對象在隊列中的位置就會發生改變——即提到隊列的頭部,假設隊列有 n 個元素,n-1 個元素都被不同頻率地訪問過,唯獨元素 A 一直沒有被訪問,那麼 A 必然處於隊尾,成為“近期最少使用元素”了。
LruCache 的實現原理LruCache 初步源碼分析
代碼太多,我不可能全都貼上來,所以我就按照我的思路來貼啦~
public class LruCache<K, V> { 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; private int createCount; private int evictionCount; private int hitCount; private int missCount;
從代碼裡我們可以知道:LruCache 不是其他類的子類,並且其中實際涉及到的只有一個 LinkedHashMap 對象和一堆 int 值。也就是說,LruCache 實現其核心邏輯的關鍵應該就是 LinkedHashMap。
那我們就先來看看 LinkedHashMap的源碼吧。
LinkedHashMap 剖析LinkedHashMap 是什麼
同樣的,引用官方的解釋:
LinkedHashMap is an implementation of Map that guarantees iteration order. All optional operations are supported.
Entries are kept in a doubly-linked list. The iteration order is, by default, the order in which keys were inserted. Reinserting an already-present key doesn’t change the order. If the three argument constructor is used, and accessOrder is specified as true, the iteration will be in the order that entries were accessed. The access order is affected by put, get, and putAll operations, but not by operations on the collection views.
LinkedHashMap 是 Map 的一種實現(HashMap 的子類,HashMap 的父類是抽象 Map 類,其中實現了 Map 介面),能夠保證迭代器的順序。LinkedHashMap 中的資料通過一個雙向鏈表格儲存體,預設情況下,迭代器的順序為鍵的摻入順序,並且重新插入一個已經存在的鍵不會改變迭代器的順序。
如果構造方法中的第三個參數 boolean accessOrder 被使用,而且 accessOrder 的值為 true,那麼迭代器的順序就是資料的訪問順序,並且,這個順序將受 put,get,putall 這些方法影響,但不受集合視圖操作的影響。
LinkedHashmap 的解釋證明了我們剛剛的猜測 LruCache 實現其核心邏輯的關鍵應該就是 LinkedHashMap 是對的,那麼我們只要弄懂了 LinkedHashpMap 的具體原理,LruCache 的原理也不是問題了。
LinkedHashMap 使用範例
在分析 LinkedHashMap 之前,我們不妨先看看 LinkedHashMap 到底能幹些什麼,畢竟從他的外在表現去分析內在邏輯會更輕鬆些,下面是一段簡單的 Java 代碼:
public class Demo { public static void main(String[] args) { LinkedHashMap<String, String> accessOrderMap = new LinkedHashMap<>(10, 0.75f, true); System.out.println("處理前"); //受訪問順序影響的LinkedHashMap測試 accessOrderMap.put("one", "one"); accessOrderMap.put("two", "two"); accessOrderMap.put("three", "three"); //未經過任何處理的輸出 for(Entry<String, String> entry : accessOrderMap.entrySet()){ System.out.println(entry.getValue()); } System.out.println("處理後"); //訪問Map中的惡元素,改變順序 accessOrderMap.get("one"); //經過處理的輸出 for(Entry<String, String> entry : accessOrderMap.entrySet()){ System.out.println(entry.getValue()); } }}
這是輸出:
處理前
one
two
three
處理後
two
three
one
我們可以看到,處理後 one 的位置顯然發生了改變,跑到了 two,three 的前面。
LinkedHashMap 實現原理
我們直接看 LinkedHashMap 中相關的第三個構造方法吧:
public LinkedHashMap( int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); init(); this.accessOrder = accessOrder; }
@Override void init() { header = new LinkedEntry<K, V>(); }
我們可以看到,構造方法調用了父類的構造方法,建立了一個 LinkedEntry 對象,並把 accessOrder 的值設為構造方法傳入的值。那這個 LinkedEntry 是什麼呢?
/** * 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 就是用於儲存資料的雙向鏈表。所以,我們現在只需要知道 LinkedHashMap 類是如何根據 accessOrder 的值,決定程式使用 get,put 等方法將如何操作 LinkedEntry 就可以知道一整個 LinkedHashMap 的實現原理了。
我們追蹤 accessOrder 的值很容易發現在 get 方法中有相應的處理:
/** * Returns the value of the mapping with the specified key. * * @param key * the key. * @return the value of the mapping with the specified key, or {@code null} * if no mapping for the specified key is found. */ @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; }
從代碼中可以看到,只要 accessOrder 為 true,就會執行 makeTail 方法操作一個建立的 LinkedEntry 對象,我們不妨進入 makeTail 方法一探究竟:
/** * Relinks the given entry to the tail of the list. Under access ordering, * this method is invoked whenever the value of a pre-existing entry is * read by Map.get or modified by Map.put. */ 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 帶來的隊列變化我們是知道了,那 put 呢?我們一搜尋 put,發現類裡沒有這個方法,這是什麼鬼……莫慌,我們耐心地翻翻代碼會發現 addNewEntry(K key, V value, int hash, int index) 和 addNewEntryForNullKey(V value) 方法,而它們就是實現 put 改變隊列邏輯的兩個方法了,我們不妨看看:
@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; } @Override void addNewEntryForNullKey(V value) { 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>( null, value, 0, null, header, oldTail); entryForNullKey = oldTail.nxt = header.prv = newTail; }
addNewEntry 方法中,當有新元素加入時就會執行進行判斷,決定是否移除到期的元素,當我們進入 removeEldestEntry 方法會發現,removeEldestEntry 方法的傳回值恒為 false,也就是說到期元素無論如何都不會被移除,那這是什麼意思呢?
其實這挺好理解的,LinkedHashMap 作為一個儲存結構,它並沒有限制自身的儲存容量(因為他的實現基於一個雙向鏈表),所以他沒有理由在預設實現中移除到期元素,而 LruCache 中則是自己設計了相應的邏輯去處理到期元素。
再探 LruCache
通過剛剛的分析,我們已經知道 LruCache 的核心 LinkedHashMap 是如何提取“近期最少使用對象”的了,那麼 LruCache 中還利用 LinkedHashMap 的特性做了什麼事呢?大家跟著我繼續探索吧~
通過 get/put 方法我們可以知道,LruCache 是通過 trimToSize 來控制其中的元素數量的,當達到數量上限,添加元素則會將到期元素移除:
private 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) { break; } // BEGIN LAYOUTLIB CHANGE // get the last item in the linked list. // This is not efficient, the goal here is to minimize the changes // compared to the platform version. Map.Entry<K, V> toEvict = null; for (Map.Entry<K, V> entry : map.entrySet()) { toEvict = entry; } // END LAYOUTLIB CHANGE if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } }
代碼首先作一些簡單判斷,只有 size > maxSize 才會執行相關邏輯:遍曆 LinkedHashMap 雙向鏈表的元素,取得隊列尾部的元素(就是我們所說的到期元素),如果它不為空白,就將它移除。
注意事項
1、如果你的緩衝對象持有需要被精確回收的資源,重寫 entryRemoved 方法能完成你的需求。
2、預設情況下,緩衝空間的大小由元素的數量決定,但你可以在不同的單元中重寫 sizeOf 方法改變大小。例如:bitmap 對象大小為 4m
int cacheSize = 4 * 1024 * 1024; // 4MiB LruCache bitmapCache = new LruCache(cacheSize) { protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }}
3、傳入 LruCache 的索引值對不能是 null,因為 get/put/remove 方法的含義會因此變得模糊。