一、基本概念
1.1 LruCache 的作用
LruCache
的基本思想是Least Recently Used
,即 最近最少使用,也就是當LruCache
內部快取在記憶體中的物件大小之和到達設定的閾值後,會刪除 訪問時間距離當前最久 的物件,從而避免了OOM
的發生。
LruCache
特別適用於圖片記憶體快取這種有可能需要佔用很多記憶體,但是隻有最近使用的物件才有可能用到的場景。
1.2 LruCache 的使用
下面,我們用一個例子來演示一下LruCache
的使用,讓大家有一個初步的認識。
public class LruCacheSamples {
private static final int MAX_SIZE = 50;
public static void startRun() {
LruCacheSample sample = new LruCacheSample();
Log.d("LruCacheSample", "Start Put Object1, size=" + sample.size());
sample.put("Object1", new Holder("Object1", 10));
Log.d("LruCacheSample", "Start Put Object2, size=" + sample.size());
sample.put("Object2", new Holder("Object2", 20));
Log.d("LruCacheSample", "Start Put Object3, size=" + sample.size());
sample.put("Object3", new Holder("Object3", 30));
Log.d("LruCacheSample", "Start Put Object4, size=" + sample.size());
sample.put("Object4", new Holder("Object4", 10));
}
static class LruCacheSample extends LruCache<String, Holder> {
LruCacheSample() {
super(MAX_SIZE);
}
@Override
protected int sizeOf(String key, Holder value) {
return value.getSize();
}
@Override
protected void entryRemoved(boolean evicted, String key, Holder oldValue, Holder newValue) {
if (oldValue != null) {
Log.d("LruCacheSample", "remove=" + oldValue.getName());
}
if (newValue != null) {
Log.d("LruCacheSample", "add=" + newValue.getName());
}
}
}
static class Holder {
private String mName;
private int mSize;
Holder(String name, int size) {
mName = name;
mSize = size;
}
public String getName() {
return mName;
}
public int getSize() {
return mSize;
}
}
}
複製程式碼
執行結果為:
在放入Object3
之後,由於放入之前LruCache
的大小為30
,而Object3
的大小為30
,放入之後的大小為60
,超過了最先設定的最大值50
,因此會移除最先插入的Object1
,減去該元素的大小10
,最新的大小變為50
。
二、原始碼解析
2.1 建構函式
首先看一下LruCache
的建構函式:
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
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);
}
複製程式碼
當我們建立一個LruCache
類時需要指定一個最大的閾值maxSize
,而我們的物件會快取在LinkedHashMap
當中:
maxSize
等於LinkedHashMap
中每個元素的sizeOf(key, value)
之和,預設情況下每個物件的大小為1
,使用者可以通過重寫sizeOf
指定對應元素的大小。LinkedHashMap
是實現LRU
演算法的核心,它會根據物件的使用情況維護一個雙向連結串列,其內部的header.after
指向歷史最悠久的元素,而header.before
指向最年輕的元素,這一“年齡”的依據可以是訪問的順序,也可以是寫入的順序。
2.2 put 流程
接下來看一下與put
相關的方法:
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
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++;
//獲得該物件的大小,由 LruCache 的使用者來決定,要求返回值大於等於 0,否則丟擲異常。
size += safeSizeOf(key, value);
//呼叫的是 HashMap 的 put 方法,previous 是之前該 key 值存放的物件。
previous = map.put(key, value);
//如果已經存在,由於它現在被替換成了新的 value,所以需要減去這個大小。
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
//通知使用者該物件被移除了。
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//由於放入了新的物件,因此需要確保目前總的容量沒有超過設定的閾值。
trimToSize(maxSize);
return previous;
}
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* Returns the size of the entry for {@code key} and {@code value} in
* user-defined units. The default implementation returns 1 so that size
* is the number of entries and max size is the maximum number of entries.
*
* <p>An entry's size must not change while it is in the cache.
*/
protected int sizeOf(K key, V value) {
//預設情況下,每個物件的權重值為 1。
return 1;
}
/**
* Remove the eldest entries until the total of remaining entries is at or
* below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1
* to evict even 0-sized elements.
*/
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!");
}
//這是一個 while 迴圈,因此將一直刪除最悠久的結點,直到小於閾值。
if (size <= maxSize) {
break;
}
//獲得歷史最悠久的結點。
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
//從 map 中將它移除。
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
//通知使用者該物件被移除了。
entryRemoved(true, key, value, null);
}
}
複製程式碼
關於程式碼的解釋都在註釋中了,其核心的思想就是在每放入一個元素之後,通過sizeOf
來獲得這個元素的權重值,如果發現所有元素的權重值之和大於size
,那麼就通過trimToSize
移除歷史最悠久的元素,並通過entryRemoved
回撥給LruCache
的實現者。
2.3 get 流程
/**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
* be created.
*/
public final V get(K key) {
//與 HashMap 不同,LruCache 不允許 key 值為空。
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//首先在 map 中查詢,如果找到了就直接返回。
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
//如果在 map 中沒有找到,get 方法不會直接返回 null,而是先回撥 create 方法,讓使用者有一個建立的機會。
V createdValue = create(key);
//如果使用者沒有重寫 create 方法,那麼會返回 null。
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
//由於 create 的過程沒有加入同步塊,因此有可能在建立的過程中,使用者通過 put 方法在 map 相同的位置放入了一個物件,這個物件是 mapValue。
mapValue = map.put(key, createdValue);
//如果存在上面的情況,那麼會拋棄掉 create 方法建立物件,重新放入已經存在於 map 中的物件。
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;
}
}
複製程式碼
這裡需要特別說明一下LruCache
與HashMap
的get
方法的區別:如果LinkedHashMap
中不存在Key
對應的Value
,get
方法並像HashMap
一樣直接返回,而是先 通過create
方法嘗試讓使用者重新建立一個物件,如果建立成功,那麼將會把這個物件放入到集合當中,並返回這個新建立的物件。
上面這種是單執行緒的情況,如果在多執行緒的情況下,由於create
方法沒有加入synchronized
關鍵字,因此有可能 一個執行緒在create
方法建立物件的過程中,另一個執行緒又通過put
方法在Key
對應的相同位置放入一個物件,在這種情況下,將會拋棄掉由create
建立的物件,維持原有的狀態。
2.4 LinkedHashMap
通過get/set
方法,我們可以知道LruCache
是通過trimToSize
來保證它所維護的物件的權重之和不超過maxSize
,最後我們再來分析一下LinkedHashMap
,看下它是如何保證每次大小超過maxSize
時,移除的都是歷史最悠久的元素的。
LinkedHashMap
繼承於HashMap
,它通過重寫相關的方法在HashMap
的基礎上實現了雙向連結串列的特性。
2.4.1 Entry 元素
LinkedHashMap
重新定義了HashMap
陣列中的HashMapEntry
,它的實現為LinkedHashMapEntry
,除了原有的next
、key
、value
和hash
值以外,它還額外地儲存了after
和before
兩個指標,用來實現根據寫入順序或者讀取順序來排列的雙向連結串列。
private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<K,V> next) {
super(hash, key, value, next);
}
//刪除連結串列結點。
private void remove() {
before.after = after;
after.before = before;
}
//在 existingEntry 之前插入該結點。
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
//如果是按訪問順序排列,那麼將該結點插入到整個連結串列的頭部。
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
//從連結串列中移除該結點。
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
複製程式碼
2.4.2 初始化
LinkedHashMap
重寫了init()
方法,該方法會在其父類HashMap
的建構函式中被呼叫,在init()
方法中,會初始化一個空的LinkedHashMapEntry
結點header
,它的before
指向最年輕的元素,而after
指向歷史最悠久的元素。
void init() {
header = new LinkedHashMapEntry<>(-1, null, null, null);
header.before = header.after = header;
}
複製程式碼
在LinkedHashMap
的建構函式中,可以傳入accessOrder
,如果accessOrder
為true
,那麼“歷史最悠久”的元素表示的是訪問時間距離當前最久的元素,即按照訪問順序排列;如果為false
,那麼表示最先插入的元素,即按照插入順序排列,預設的值為false
。
2.4.3 元素寫入
對於元素的寫入,LinkedHashMap
並沒有重寫put
方法,而是重寫了addEntry/createEntry
方法,在建立結點的同時,更新它所維護的雙向連結串列。
void addEntry(int hash, K key, V value, int bucketIndex) {
LinkedHashMapEntry<K,V> eldest = header.after;
if (eldest != header) {
boolean removeEldest;
size++;
try {
removeEldest = removeEldestEntry(eldest);
} finally {
size--;
}
if (removeEldest) {
removeEntryForKey(eldest.key);
}
}
super.addEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> old = table[bucketIndex];
LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
複製程式碼
2.4.4 元素讀取
對於元素的讀取,LinkedHashMap
重寫了get
方法,它首先呼叫HashMap
的getEntry
方法找到結點,如果判斷是需要根據訪問的順序來排列雙向列表,那麼就需要對連結串列進行更新,即呼叫我們在2.4.1
中看到的recordAccess
方法。
public V get(Object key) {
LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
複製程式碼