秒懂 LruCache
目的
在 Android 開發中,我們需要避免程式佔用過多的記憶體資源或者儲存空間,比如網路載入圖片下載檔案等,當快取大小達到一定值的時候我們需要從快取中釋放空間存放新的快取,LruCache 就是常用的用於管理快取的類。
LruCache 快取演算法
Lru: Least Recently Used
原理: 當快取大小大於最大值時,優先拋棄快取中最近最少使用的快取,直到當前快取大小小於等於最大值。
LruCache LRU 實現
利用 LinkedHashMap 可以根據元素呼叫排序的特點,提供 get() put() remove() 等方法來操作快取。
原始碼解析
構造方法:
// LruCache.java 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); }
構造方法的核心就是建立了一個初始容量為 0 ,預設 loadFactor = 0.75f,根據元素訪問排序的 LinkedHashMap.
// LruCache.java 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; // 當前快取大小,不一定跟 map 中的鍵值對數相同 private int maxSize; // 最大快取大小 private int putCount; // 標記 put() 被呼叫次數 private int createCount; // 標記呼叫 create() 且返回不為 null 的次數 private int evictionCount; // 標記 map 中被拋棄的值的數量 private int hitCount; // 標記呼叫 get() 時存在值的次數 private int missCount; // 標記呼叫 get() 時不存在值的次數 ... public synchronized final int size() { return size; } public synchronized final int maxSize() { return maxSize; } public synchronized final int hitCount() { return hitCount; } public synchronized final int missCount() { return missCount; } public synchronized final int createCount() { return createCount; } public synchronized final int putCount() { return putCount; } public synchronized final int evictionCount() { return evictionCount; } }
核心方法:
1.get()
// LruCache.java public final V get(K key) { if (key == null) { // 非空判斷 throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { // 從快取 map 中獲取 mapValue mapValue = map.get(key); // 非空判斷 if (mapValue != null) { // 不為空,累加 hitCount 並返回 mapValue hitCount++; return mapValue; } // mapValue 為空,累加 missCount missCount++; } /* * Attempt to create a value. This may take a long time, and the map * may be different when create() returns. If a conflicting value was * added to the map while create() was working, we leave that value in * the map and release the created value. * * 建立一個 value。這裡需要消耗點時間,map 有可能在 create() 結束的時候 * 發生變化。併發情況下,如果在執行 create() 的時候有別的 value 新增到 map 中 * 的時候,會保留別的 value 在 map 中並且丟棄 createdValue. */ V createdValue = create(key); // 預設情況下返回 null if (createdValue == null) { return null; } // 同步鎖程式碼塊 synchronized (this) { // 累加 createCount createCount++; // 把 createdValue 放進 map // LinkedHashMap.put() 返回的是該 key 原本的 value (原本不存在時返回null) // 無論 value 是否為 null createdValue 依然會新增到 map 中 mapValue = map.put(key, createdValue); if (mapValue != null) { // 不為空則有別的 value 已經新增在 map 中,且重新把這個 value 放進 map 中 // There was a conflict so undo that last put map.put(key, mapValue); } else { // 新增成功後根據 createdValue 的大小新增到 size size += safeSizeOf(key, createdValue); } } if (mapValue != null) { // 已經存在別的 value // 回撥 entryRemoved() entryRemoved(false, key, createdValue, mapValue); // 返回原本存在的 value return mapValue; } else { // 新增成功 // 縮短快取大小直到小於最大快取值 trimToSize(maxSize); // 返回建立的 createdValue return createdValue; } } protected V create(K key) { return null; } private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; } protected int sizeOf(K key, V value) { return 1; } protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
直接根據 key 從快取 map 中直接獲取 value,並進行非空判斷:
- 非null : 直接返回 value,結束 get() 。
-
null : 呼叫 create() 建立一個值若為 null 直接返回 null 結束 get(),不為 null 就呼叫 map.put() 到快取中,由於存在併發的情況,所以有可能在 create() 執行過程中已經有別的 value 對應同一個 key 新增到 map 中的情況,所以根據判斷 put() 返回值是否為空可以判斷 map 中是否已經存在 key 對應的值 :
-
null: 建立的 createValue 新增成功,呼叫 safeSizeOf() 判斷 createValue 的大小並累加到
size
上,然後呼叫 trimTosize() 使快取大小小於maxSize
,最後返回 createValue。 - 非null: map 中已存在別的值 mapValue ,重新把 mapValue 放進 map 中,然後呼叫回撥方法 entryRemoved(),最後返回 mapValue。
-
null: 建立的 createValue 新增成功,呼叫 safeSizeOf() 判斷 createValue 的大小並累加到
在 get() 中有幾個 LruCache 提供給我們根據業務需求重寫的方法:
1.create()
預設直接返回 null,根據業務需求我們可以在發現 key 對應的值不存在的時候建立一個值放進快取中。
2.sizeOf()
預設值為1,根據需求我們可以設定每個值的快取大小,比如圖片我們可以返回圖片佔用的記憶體大小。
3.entryRemoved()
預設空實現,每當有 value 從 map 中被移除或者被拋棄的時候會呼叫。
2.put()
// LruCache.java public final V put(K key, V value) { if (key == null || value == null) { // 非空判斷 throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { // 同步鎖程式碼塊 // 累加 put() 呼叫次數 putCount++; // 累加快取大小 size += safeSizeOf(key, value); // 新增到 map 中 previous = map.put(key, value); // 如果該 key 對應的 value 之前已存在, // 當前快取大小就需要減掉之前的 value 大小 if (previous != null) { size -= safeSizeOf(key, previous); } } // 如果該 key 對應的 value 之前已存在, // 呼叫回撥方法 entryRemoved() if (previous != null) { entryRemoved(false, key, previous, value); } // 最後調整快取大小 trimToSize(maxSize); // 返回之前的 value 值,若 value 之前不存在則為 null return previous; }
放進快取,並返回呼叫 put() 之前快取中該 key 對應的 value。
3.trimToSize()
調整快取大小直到小於設定的最大值為止。
// LruCache.java 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) { // 若當前快取大小小於最大值,跳出迴圈方法結束 break; } // 獲取 map 最後一個鍵值對,即按照呼叫順序末尾的鍵值對 Map.Entry<K, V> toEvict = map.eldest(); if (toEvict == null) { // 若末尾值為 null,跳出迴圈方法結束 break; } // 獲取 key key = toEvict.getKey(); // 獲取 value value = toEvict.getValue(); // 從 map 中移除 map.remove(key); // 從快取大小中減去該值的大小 size -= safeSizeOf(key, value); // 累加 evictionCount evictionCount++; } // 呼叫回撥方法 entryRemoved entryRemoved(true, key, value, null); } }
每次迴圈都移除呼叫順序末尾的快取,直到當前快取大小小於設定的最大值。
4.remove()
根據 Key 從 map 中移除快取。
// LruCache.java public final V remove(K key) { if (key == null) { // 非空判斷 throw new NullPointerException("key == null"); } V previous; synchronized (this) { // 從 map 中移除,並獲取移除之前的 value previous = map.remove(key); if (previous != null) { // 若移除之前值不為 null,當前快取大小需要減去 previous 大小 size -= safeSizeOf(key, previous); } } if (previous != null) { // 呼叫回撥方法 entryRemoved entryRemoved(false, key, previous, null); } // 返回移除前的值,若移除前值不存在則返回 null return previous; }
其他方法:
1.resize()
重置 LruCache 的最大快取大小。
public void resize(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } synchronized (this) { this.maxSize = maxSize; } // 重新賦值 maxSize 後要調整令 size <= maxSize trimToSize(maxSize); }
2.snapshot()
獲取一個複製當前快取 map 的 LinkedHashMap。
public synchronized final Map<K, V> snapshot() { return new LinkedHashMap<K, V>(map); }
3.evictAll()
清除所有快取。
public final void evictAll() { // 直接呼叫 trimToSize(-1) 則最後快取大小會變成 0 trimToSize(-1); // -1 will evict 0-sized elements }