以下針對 Android API 26 版本的原始碼進行分析。
在瞭解LruCache
之前,最好對LinkedHashMap
有初步的瞭解,LruCache
的實現主要藉助LinkedHashMap
。LinkedHashMap
的原始碼解析,可閱讀Java——LinkedHashMap原始碼解析
概述
LruCahce
其 Lru 是 Least Recently Used 的縮寫,即最近最少使用,是包含對有限數量值的強引用的快取。每當一個值被訪問,它將被移到隊尾。當快取達到指定的數量時,位於隊頭的值將被移除,並且可能被 GC 回收。如果快取的值包含需要顯式釋放的資源,那麼需要重寫entryRemoved
方法。如果 key 對應的快取未命中,通過重寫create
方法建立對應的 value。這可以簡化程式碼呼叫:即使存在快取未命中,也允許假設始終返回一個值。
預設情況下,快取大小以條目數量度量。在不同快取物件下,通過重寫sizeOf
方法測量 key-value 快取的大小。例如如下的例子,這個快取限制了 4MiB 大小的點陣圖:
int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
}
複製程式碼
這個類是執行緒安全的,通過在快取上執行同步操作來以原子方式執行多個快取操作:
synchronized (cache) {
if (cache.get(key) == null) {
cache.put(key, value);
}
}
複製程式碼
這個類不允許空值作為 key 或者 value,對於get
、put
、remove
方法返回null
值是明確的行為:快取中不存在這個鍵。
原始碼分析
主要欄位
//LruCache 主要藉助 LinkedHashMap 按元素訪問順序的迭代順序(此時 accessOrder = true)來實現
private final LinkedHashMap<K, V> map;
/** 不同 key-value 條目下快取的大小,不一定是 key-value 條目的數量 */
private int size;
//快取大小的最大值
private int maxSize;
//儲存的 key-value 條目的個數
private int putCount;
//建立 key 對應的 value 的次數
private int createCount;
//快取移除的次數
private int evictionCount;
//快取命中的次數
private int hitCount;
//快取未命中的次數
private int missCount;
複製程式碼
建構函式
/**
* maxSize 對於快取沒有重寫 sizeOf 方法的時候,這個數值指定了快取中可以容納的最大條目的數量;
* 對於其他快取,這是快取中條目大小的最大總和。
*/
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
//指定了雜湊表初始容量為0,負載因子為0.75,迭代順序為按照條目訪問順序
//因此在有對條目進行訪問的操作的時候,條目都會被放置到隊尾,具體細節詳看 LinkedHashMap 的解析
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
複製程式碼
Size操作
LruCache
在預設情況下,size 指的是 key-value 條目的個數,當重寫sizeOf
函式時,可以自定義 key-value 條目的單位大小,如概述中點陣圖的例子,其通過重寫sizeOf
函式,返回的大小值並非是 1,而是不同Bitmap
物件的位元組大小。
/**
* 以使用者定義的單位返回 key-value 條目的大小
* 預設實現返回1,因此 size 是條目數,max size是最大條目數
* 條目的大小在快取中時不得更改
*/
protected int sizeOf(K key, V value) {
return 1;
}
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
/**
* 刪除最舊的條目,直到剩餘條目總數小於等於指定的大小。
*/
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.Entry<K, V> toEvict = map.eldest();
//雜湊表為空,終止
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
//移除元素的次數
evictionCount++;
}
//此處 evicted 為 true,表明是為了騰出空間而進行的刪除條目操作
entryRemoved(true, key, value, null);
}
}
/**
* 調整快取的大小
*/
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
複製程式碼
查詢
/**
* 指定 key 對應的 value 值存在時返回,否則通過 create 方法建立相應的 key-value 對。
* 如果對應的 value 值被返回,那麼這個 key-value 對將被移到隊尾。
* 當返回 null 時,表明沒有對應的 value 值並且也無法被建立
*/
public final V get(K key) {
//快取不允許 key 值為 null,因此對於查詢 null 的鍵可直接丟擲異常
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
//快取命中
if (mapValue != null) {
hitCount++;
return mapValue;
}
//快取未命中
missCount++;
}
/*
* 嘗試建立一個 value 值,這可能需要花費較長的時間完成,當 create 返回時,雜湊表可能變得不同
* 如果在 create 工作時向雜湊表新增了一個衝突的值(key 已經有對應的 value 值,但 create 方法返回了一個不同的 value 值)
* 那麼將該值保留在雜湊表中並釋放建立的值。
*/
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
//快取建立的次數
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// mapValue 不為 null,說明存在一個衝突值,保留之前的 value 值
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 的 value。
* 當能計算 key 對應的 value 時,返回 value,否則返回 null。預設實現一律返回 null 值。
*
* 這個方法在被呼叫的時候沒有新增額外的同步操作,因此其他執行緒可能在這個方法執行時訪問快取
*
* 如果 key 對應的 value 儲存在快取中,那麼通過 create 建立的 value 將通過 entryRemoved 方法釋放。
* 這種情況主要發生在:當多個執行緒同時請求相同的 key (導致建立多個值)時,或者當一個執行緒呼叫 put 而另一個執行緒為其建立值時
*/
protected V create(K key) {
return null;
}
複製程式碼
儲存
/**
* 對於 key,快取其相應的 value,key-value 條目放置於隊尾
*
* @return 返回先前 key 對應的 value 值
*/
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) {
//evicted 為 true,表明不是為了騰出空間而進行的刪除操作
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
複製程式碼
刪除
/**
* 刪除 key 對應的條目
*
* 返回 key 對應的 value值
*/
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
/**
* 當條目需要被移除或刪除時呼叫
* 當一個值被移除以騰出空間,通過呼叫 remove 刪除,或者被 put 呼叫替換值時,會呼叫此方法。預設實現什麼也不做
*
* 這個方法在被呼叫的時候沒有新增額外的同步操作,因此其他執行緒可能在這個方法執行時訪問快取
*
* @param evicted true 表明條目正在被刪除以騰出空間,false 表明刪除是由 put 或 remove 引起的(並非是為了騰出空間)
*
* @param newValue key 的新值。如果非 null,則此刪除是由 put 引起的。否則它是由 remove引起的
*/
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
複製程式碼