30.Android v4 LruCache
1. 簡介
LRU 是 Least Recently Used 最近最少使用算法。
曾經,在各大緩存圖片的框架沒流行的時候。有一種很常用的內存緩存技術:SoftReference 和 WeakReference(軟引用和弱引用)。但是走到了 Android 2.3(Level 9)時代,垃圾回收機制更傾向於回收 SoftReference 或 WeakReference 的對象。後來,又來到了 Android3.0,圖片緩存在內容中,因爲不知道要在是什麼時候釋放內存,沒有策略,沒用一種可以預見的場合去將其釋放。這就造成了內存溢出。
2. 使用方法
當成一個 Map 用就可以了,只不過實現了 LRU 緩存策略。
使用的時候記住幾點即可:
- 1.(必填)你需要提供一個緩存容量作爲構造參數。
- 2.(必填) 覆寫 sizeOf
方法 ,自定義設計一條數據放進來的容量計算,如果不覆寫就無法預知數據的容量,不能保證緩存容量限定在最大容量以內。
- 3.(選填) 覆寫 entryRemoved
方法 ,你可以知道最少使用的緩存被清除時的數據( evicted, key, oldValue, newVaule )。
- **4.(記住)**LruCache是線程安全的,在內部的 get、put、remove 包括 trimToSize 都是安全的(因爲都上鎖了)。
- 5.(選填) 還有就是覆寫 create
方法。
一般做到 1、2、3、4就足夠了,5可以無視 。
以下是 一個 LruCache 實現 Bitmap 小緩存的案例, entryRemoved
裏的自定義邏輯可以無視,這裏是我的展示 demo 裏的自定義 entryRemoved
邏輯。
private static final float ONE_MIB = 1024 * 1024;
// 7MB
private static final int CACHE_SIZE = (int) (7 * ONE_MIB);
private LruCache<String, Bitmap> bitmapCache;
this.bitmapCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
/**
* 1.當被回收或者刪掉時調用。該方法當value被回收釋放存儲空間時被remove調用
* 或者替換條目值時put調用,默認實現什麼都沒做。
* 2.該方法沒用同步調用,如果其他線程訪問緩存時,該方法也會執行。
* 3.evicted=true:如果該條目被刪除空間 (表示 進行了trimToSize or remove) evicted=false:put衝突後 或 get裏成功create後 導致
* 4.newValue!=null,那麼則被put()或get()調用。
*/
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
mEntryRemovedInfoText.setText(
String.format(Locale.getDefault(), LRU_CACHE_ENTRY_REMOVED_INFO_FORMAT,
evicted, key, oldValue != null ? oldValue.hashCode() : "null",
newValue != null ? newValue.hashCode() : "null"));
// 見上述 3.
if (evicted) {
// 進行了trimToSize or remove (一般是溢出了 或 key-value被刪除了 )
if (recentList.contains(key)) {
recentList.remove(key);
refreshText(mRecentInfoText, LRU_CACHE_RECENT_FORMAT, recentList);
}
} else {
// put衝突後 或 get裏成功create 後
recentList.remove(key);
refreshText(mRecentInfoText, LRU_CACHE_RECENT_FORMAT, recentList);
}
if (cacheList.contains(key)) {
cacheList.remove(key);
refreshText(mCacheDataText, LRU_CACHE_CACHE_DATA_FORMAT, cacheList);
}
}
};
3. 效果展示
3.1 效果一(驗證 LRU,最近沒訪問的,在溢出時優先被清理)
前提: 設置 LruCache 最大容量爲 7MB,把圖1、2、3放入了,此時佔用容量爲:1.87+0.38+2.47=4.47MB。
執行操作:
- 1.然後點 get 圖3一共16次(證明訪問次數和 LRU 沒關係,只有訪問順序有關係)。Recent visit顯示了圖3。
- 2.先 get 圖2,再 get 圖1,製造最近訪問順序爲:<1> <2> <3>。
- 3. put 圖4,預算容量需要4.47+2.47=7.19MB。會溢出。
- 4.溢出了,刪除最近沒訪問的圖3。
- 5.觀察 entryRemoved
數據 圖三被移除了(對照hashcode)。
3.2 效果二(驗證 entryRemoved 的 evicted=false,可以驗證衝突)
前提:執行了效果一,put 了圖4,刪除了最近沒訪問的圖3。
執行操作:再一次 put 圖4,發生衝突,拿到 key、衝突 value 以及 put 的 value,這裏我放到是同一個 hashcode 的 bitmap,所以 hashcode 一樣,但是無關緊要吧。
4. 源碼分析
4.1 LruCache 原理概要解析
LruCache 就是 利用 LinkedHashMap 的一個特性( accessOrder=true 基於訪問順序 )再加上對 LinkedHashMap 的數據操作上鎖實現的緩存策略。
LruCache 的數據緩存是內存中的。
1.首先設置了內部
LinkedHashMap
構造參數accessOrder=true
, 實現了數據排序按照訪問順序。2.然後在每次
LruCache.get(K key)
方法裏都會調用LinkedHashMap.get(Object key)
。3.如上述設置了
accessOrder=true
後,每次LinkedHashMap.get(Object key)
都會進行LinkedHashMap.makeTail(LinkedEntry<K, V> e)
。4.
LinkedHashMap
是雙向循環鏈表,然後每次LruCache.get
->LinkedHashMap.get
的數據就被放到最末尾了。5.在
put
和trimToSize
的方法執行下,如果發成數據量移除了,會優先移除掉最前面的數據(因爲最新訪問的數據在尾部)。
具體解析在: 4.2、4.3、4.4、4.5 。
4.2 LruCache 的唯一構造方法
/**
* LruCache的構造方法:需要傳入最大緩存個數
*/
public LruCache(int maxSize) {
// 最大緩存個數小於0,會拋出IllegalArgumentException
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
/*
* 初始化LinkedHashMap
* 第一個參數:initialCapacity,初始大小
* 第二個參數:loadFactor,負載因子=0.75f
* 第三個參數:accessOrder=true,基於訪問順序;accessOrder=false,基於插入順序
*/
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
第一個參數 initialCapacity
用於初始化該 LinkedHashMap 的大小。
先簡單介紹一下 第二個參數 loadFactor
,這個其實的 HashMap 裏的構造參數,涉及到擴容問題,比如 HashMap 的最大容量是100,那麼這裏設置0.75f的話,到75容量的時候就會擴容。
主要是第三個參數 accessOrder=true
,這樣的話 LinkedHashMap 數據排序就會基於數據的訪問順序,從而實現了 LruCache 核心工作原理。
4.3 LruCache.get(K key)
/**
* 根據 key 查詢緩存,如果存在於緩存或者被 create 方法創建了。
* 如果值返回了,那麼它將被移動到雙向循環鏈表的的尾部。
* 如果如果沒有緩存的值,則返回 null。
*/
public final V get(K key) {
...
V mapValue;
synchronized (this) {
// LinkHashMap 如果設置按照訪問順序的話,這裏每次get都會重整數據順序
mapValue = map.get(key);
// 計算 命中次數
if (mapValue != null) {
hitCount++;
return mapValue;
}
// 計算 丟失次數
missCount++;
}
/*
* 官方解釋:
* 嘗試創建一個值,這可能需要很長時間,並且Map可能在create()返回的值時有所不同。如果在create()執行的時
* 候,一個衝突的值被添加到Map,我們在Map中刪除這個值,釋放被創造的值。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
/***************************
* 不覆寫create方法走不到下面 *
***************************/
/*
* 正常情況走不到這裏
* 走到這裏的話 說明 實現了自定義的 create(K key) 邏輯
* 因爲默認的 create(K key) 邏輯爲null
*/
synchronized (this) {
// 記錄 create 的次數
createCount++;
// 將自定義create創建的值,放入LinkedHashMap中,如果key已經存在,會返回 之前相同key 的值
mapValue = map.put(key, createdValue);
// 如果之前存在相同key的value,即有衝突。
if (mapValue != null) {
/*
* 有衝突
* 所以 撤銷 剛纔的 操作
* 將 之前相同key 的值 重新放回去
*/
map.put(key, mapValue);
} else {
// 拿到鍵值對,計算出在容量中的相對長度,然後加上
size += safeSizeOf(key, createdValue);
}
}
// 如果上面 判斷出了 將要放入的值發生衝突
if (mapValue != null) {
/*
* 剛纔create的值被刪除了,原來的 之前相同key 的值被重新添加回去了
* 告訴 自定義 的 entryRemoved 方法
*/
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
// 上面 進行了 size += 操作 所以這裏要重整長度
trimToSize(maxSize);
return createdValue;
}
}
上述的 get
方法表面並沒有看出哪裏有實現了 LRU 的緩存策略。主要在 mapValue = map.get(key)
;裏,調用了 LinkedHashMap 的 get 方法,再加上 LruCache 構造裏默認設置 LinkedHashMap 的 accessOrder=true。
4.4 LinkedHashMap.get(Object key)
/**
* 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;
}
其實仔細看 if (accessOrder)
的邏輯即可,如果 accessOrder=true
那麼每次 get
都會執行 N 次 makeTail(LinkedEntry<K, V> e)
。
接下來看看
4.5 LinkedHashMap.makeTail(LinkedEntry
/**
* 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++;
}
// Unlink e
// Relink e as tail
LinkedHashMap 是雙向循環鏈表,然後此次 LruCache.get -> LinkedHashMap.get 的數據就被放到最末尾了。
以上就是 LruCache 核心工作原理。
接下來介紹 LruCache 的容量溢出策略。
上述展示場景中,7M的容量,我添加三張圖後,不會溢出,put<4>後必然會超過7MB。
4.6 LruCache.put(K key, V value)
public final V put(K key, V value) {
...
synchronized (this) {
...
// 拿到鍵值對,計算出在容量中的相對長度,然後加上
size += safeSizeOf(key, value);
...
}
...
trimToSize(maxSize);
return previous;
}
記住幾點:
- 1.*put 開始的時候確實是把值放入 LinkedHashMap 了,不管超不超過你設定的緩存容量*。
- 2.然後根據 safeSizeOf
方法計算 此次添加數據的容量是多少,並且加到 size
裏 。
- 3.說到 safeSizeOf
就要講到 sizeOf(K key, V value)
會計算出此次添加數據的大小 (像上面的 Demo,我的容量是7MB,我每次添加進來的 Bitmap 要是不覆寫 sizeOf 方法的話,會視爲該 bitmap 的容量計算爲默認的容量計算 return 1。如此一來,這樣的話 7MB 的 LruCache 容量可以放7x1024x1024張圖片?明顯這樣的邏輯是不對的!)。
- 4.直到 put 要結束時,進行了 trimToSize
才判斷 size
是否 大於 maxSize
然後進行最近很少訪問數據的移除。
4.7 LruCache.trimToSize(int maxSize)
public void trimToSize(int maxSize) {
/*
* 這是一個死循環,
* 1.只有 擴容 的情況下能立即跳出
* 2.非擴容的情況下,map的數據會一個一個刪除,直到map裏沒有值了,就會跳出
*/
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!");
}
// 如果是 擴容 或者 map爲空了,就會中斷,因爲擴容不會涉及到丟棄數據的情況
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);
}
}
簡單描述:會判斷之前 size
是否大於 maxSize
。是的話,直接跳出後什麼也不做。不是的話,證明已經溢出容量了。由 makeTail
圖已知,最近經常訪問的數據在最末尾。拿到一個存放 key 的 Set,然後一直一直從頭開始刪除,刪一個判斷是否溢出,直到沒有溢出。
最後看看:
4.8 覆寫 entryRemoved 的作用
entryRemoved被LruCache調用的場景:
- 1.(put) put 發生 key 衝突時被調用,evicted=false,key=此次 put 的 key,oldValue=被覆蓋的衝突 value,newValue=此次 put 的 value。
- 2.(trimToSize) trimToSize 的時候,只會被調用一次,就是最後一次被刪除的最少訪問數據帶回來。evicted=true,key=最後一次被刪除的 key,oldValue=最後一次被刪除的 value,newValue=null(此次沒有衝突,只是 remove)。
- 3.(remove) remove的時候,存在對應 key,並且被成功刪除後被調用。evicted=false,key=此次 put的 key,oldValue=此次刪除的 value,newValue=null(此次沒有衝突,只是 remove)。
- 4.(get後半段,查詢丟失後處理情景,不過建議忽略) get 的時候,正常的話不實現自定義 create
的話,代碼上看 get 方法只會走一半,如果你實現了自定義的 create(K key)
方法,並且在 你 create 後的值放入 LruCache 中發生 key 衝突時被調用,evicted=false,key=此次 get 的 key,oldValue=被你自定義 create(key)後的 value,newValue=原本存在 map 裏的 key-value。
解釋一下第四點吧:<1>.第四點是這樣的,先 get(key),然後沒拿到,丟失。<2>.如果你提供了 自定義的 create(key)
方法,那麼 LruCache 會根據你的邏輯自造一個 value,但是當放入的時候發現衝突了,但是已經放入了。<3>.此時,會將那個衝突的值再讓回去覆蓋,此時調用上述4.的 entryRemoved。
因爲 HashMap 在數據量大情況下,拿數據可能造成丟失,導致前半段查不到,你自定義的 create(key)
放入的時候發現又查到了(有衝突)。然後又急忙把原來的值放回去,此時你就白白create一趟,無所作爲,還要走一遍entryRemoved。
綜上就如同註釋寫的一樣:
/**
* 1.當被回收或者刪掉時調用。該方法當value被回收釋放存儲空間時被remove調用
* 或者替換條目值時put調用,默認實現什麼都沒做。
* 2.該方法沒用同步調用,如果其他線程訪問緩存時,該方法也會執行。
* 3.evicted=true:如果該條目被刪除空間 (表示 進行了trimToSize or remove) evicted=false:put衝突後 或 get裏成功create後
* 導致
* 4.newValue!=null,那麼則被put()或get()調用。
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}
可以參考我的 demo 裏的 entryRemoved
。
4.9 LruCache 局部同步鎖
在 get
, put
, trimToSize
, remove
四個方法裏的 entryRemoved
方法都不在同步塊裏。因爲 entryRemoved
回調的參數都屬於方法域參數,不會線程不安全。
本地方法棧和程序計數器是線程隔離的數據區。
5. 開源項目中的使用
6. 總結
LruCache重要的幾點:
1.*LruCache 是通過 LinkedHashMap 構造方法的第三個參數的
accessOrder=true
實現了LinkedHashMap
的數據排序基於訪問順序* (最近訪問的數據會在鏈表尾部),在容量溢出的時候,將鏈表頭部的數據移除。從而,實現了 LRU 數據緩存機制。**2.**LruCache 在內部的get、put、remove包括 trimToSize 都是安全的(因爲都上鎖了)。
**3.**LruCache 自身並沒有釋放內存,將 LinkedHashMap 的數據移除了,如果數據還在別的地方被引用了,還是有泄漏問題,還需要手動釋放內存。
4.覆寫
entryRemoved
方法能知道 LruCache 數據移除是是否發生了衝突,也可以去手動釋放資源。5.
maxSize
和sizeOf(K key, V value)
方法的覆寫息息相關,必須相同單位。( 比如 maxSize 是7MB,自定義的 sizeOf 計算每個數據大小的時候必須能算出與MB之間有聯繫的單位 )。