簡介
LruCache是Android 3.1的時候出現的,一般我們爲了兼容低版本會使用v4包下的。LruCache是一種緩存策略,持有的是強引用,但是會控制在一個峯值下。它內部維護了一個隊列,每當從中取出一個值時,該值就移動到隊列的頭部。當緩存已滿而繼續添加時,會將隊列尾部的值移除,方便GC。LruCache用於內存緩存,在避免程序發生OOM和提高執行效率有着良好表現。
LRU算法
和名字一樣,LruCache的實現正是基於LRU(Least Recently Used)算法。最近最少使用,我理解的就是最久遠的最少使用先被淘汰。下圖展示了LRU算法的核心思想,是最常用也是比較簡單的一種:
假設一個隊列的最大容量是5,那麼新進的元素會被添加到頭部,當隊列已滿時繼續添加會移除尾部的元素。值得注意的是,如果有一個不在隊頭的元素C又一次插入到隊列,因爲隊列中已經存在C,則不會重複插入,而是將C元素移動到頭部,相當於它的存在優先級當前是最高的。
LinkedHashMap
查看LruCache的源碼很容易就發現,只有一個容器類,就是LinkedHashMap。正好LinkedHashMap是一個雙向鏈表的數據結構,分爲訪問順序和插入順序。而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);
}
在這裏初始化了LinkedHashMap,並且是按訪問順序排序。也就是說,遍歷該隊列的entrySet,最先輸出的總是最早被插入並且最近沒有被訪問(操作)過的。最後被訪問的元素一定是作爲隊尾輸出的。要注意這裏的隊頭隊尾,和LruCache描述的隊列以及上圖中的隊列的區別,比較容易讓人矇蔽。
LruCache的初始化
以下是比較重要的幾個成員變量及代表的含義:
private int size; //當前緩存的大小
private int maxSize; //最大可緩存的大小
private int hitCount; //命中緩存的次數
private int missCount; //丟失緩存的次數
LruCache的構造函數中傳入參數爲可緩存的最大容量並賦值給maxSize,如果在初始化該LruCache對象時沒有重寫其sizeOf
方法,那麼maxSize就代表了其內部的LinkedHashMap可以存儲的最大鍵值對數量。因爲sizeOf
默認返回1,代表生產了一個鍵值對。注意只有maxSize和sizeOf
返回值是同一個單位制緩存的判斷纔有意義。
put
LruCache插入元素(緩存值)的方法:
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;
}
- 異常判斷說明LruCache不允許鍵或值爲null的操作。
- 在插入元素前會調用一次
sizeOf
,前面已經說過默認返回1,但一般我們會根據實際需要重寫。比如用LruCache存儲的value爲File,那麼sizeOf
返回的就應該是當前對應該key的文件大小。 - 相應的size也要完成自增長,因爲當前緩存增加了,並且將對應的key-value插入到鏈表中。
- 二次檢查,如果該key已經存在鏈表中,此時新的value覆蓋後,size要減去之前的value所佔用的大小。
- 上面的操作都是同步的,爲了在多線程場景下保證size的準確性,否則緩存策略失效
- 如果是覆蓋了舊的value,LruCache對外提供了一個空方法
entryRemoved
。 - 調用
trimToSize
,保證緩存不溢出。
trimToSize
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);
}
}
該方法每插入一次元素就會被調用一次。整個方法就是一個無限循環,判斷當前緩存大小不大於最大容量則結束循環。否則就取出LinkedHashMap的entrySet的頭部,也就是最早被插入且最近未被訪問過的鍵值對並刪除,更新size。重複此步驟直到緩存<=最大容量。不得不說利用訪問順序的LinkedHashMap的特性完成LRU緩存,非常巧妙。
get
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
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.
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
- key不可以爲null
- 如果map中存在與key相對應的value,則返回該value,並且緩存命中數+1。不存在,則緩存丟失數+1
- 不存在的話會嘗試根據該key創建一個value。創建方法默認返回null,需要自己實現。
其它
除此之外,LruCache還提供了手動清除指定緩存remove(K key)
,清除所有緩存evictAll()
等方法供使用者調用。弄明白LinkedHashMap就很容易弄懂LruCache的實現了。