LruCache源碼解析
LruCache是Android中的一個緩存工具類,它採用了一種最近最少使用算法,可以將一些對象進行內存緩存,當緩存滿後,會優先刪除近期最少使用的對象。LruCache在實際開發中是使用率非常高的一個工具類,許多著名的圖片加載,網絡請求等框架內部都是使用的LruCache對象進行數據緩存,因此我們有必要了解LruCache內部的工作原理。
基本使用
LruCache本身是一個泛型類,使用起來也非常簡單,如果我們想要使用LruCache對象,首先要實例化一個LruCache對象,並重寫它的sizeOf方法,我們以緩存Bitmap對象爲例:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int maxSize = maxMemory / 8;
LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(maxSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
LruCache的構造方法需要傳入一個maxSize參數,這個參數代表可以緩存的最大值。而sizeOf方法用來計算要緩存的對象的大小。
如果我們想要緩存一個對象,只需要調用LruCache對象的put(K key, V value)方法即可,而當我們想要拿取一個緩存時,則需要調用get(K key)方法:
lruCache.put(key, bitmap);
Bitmap cache = lruCache.get(key);
LinkedHashMap對象
我們通過分析LruCache的源碼來分析LruCache的工作原理。首先看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);
}
我們可以看到,在構造方法中首先將maxSize參數保存在了一個成員變量中,然後初始化了一個LinkedhashMap對象。這個LinkedHashMap其實就是LruCache的核心部分,使用LruCache緩存的對象其實是存儲在這個LinkedHashMap中的。
LinkedHashMap是HashMap的一個子類,與HashMap不同的是,LinkedHashMap能夠記住每條記錄的插入順序。LinkedHashMap類有許多重載的構造方法,而在LruCache中使用的是LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)這個構造方法,其中最關鍵的就是accessOrder這個參數,它代表着LinkedhashMap中每條記錄的排序規則。當accessOrder爲false時,LinkedHashMap是按照插入順序來對每條記錄進行排序的,而當accessOrder爲true時,LinkedHashMap則會採用每條記錄的訪問順序來進行排序。
舉個例子:
Map<Integer, String> hashMap = new HashMap<>();
hashMap.put(3, "第一條");
hashMap.put(2, "第二條");
hashMap.put(1, "第三條");
System.out.println("HashMap:\n" + hashMap);
//採用這個無參的構造方法創建LinkedHashMap時,accessOrder默認爲false
Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put(3, "第一條");
linkedHashMap.put(2, "第二條");
linkedHashMap.put(1, "第三條");
System.out.println("\nLinkedHashMap:\n" + linkedHashMap);
Map<Integer, String> linkedHashMap2 = new LinkedHashMap<>(0, 0.75f, true);
linkedHashMap2.put(3, "第一條");
linkedHashMap2.put(2, "第二條");
linkedHashMap2.put(1, "第三條");
linkedHashMap2.get(2);//在這裏訪問了一下"第二條"
System.out.println("\nLinkedHashMap2:\n" + linkedHashMap2);
輸出結果:
HashMap:
{1=第三條, 2=第二條, 3=第一條}
LinkedHashMap:
{3=第一條, 2=第二條, 1=第三條}
LinkedHashMap2:
{3=第一條, 1=第三條, 2=第二條}
可以看到默認情況下LinkedHashMap是按照順序來存儲每條記錄的,先插入的在前,後插入的在後,而當accessOrder爲true時,最近訪問的記錄會被排到後面。LruCache就是根據LinkedHashMap這個特性來判斷哪些緩存數據需要被優先清除的。
插入緩存
LruCache使用put方法來插入一個緩存數據,put方法的源碼如下:
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); //將數據插入到LinkedHashMap中
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
put方法的源碼非常好理解,首先盤點key和value是否爲null,如果爲null就直接拋出異常了。然後使用了synchronized關鍵字來保證線程安全。size變量代表當前已經使用的緩存的大小,使用sizeOf方法來計算本次提交的緩存數據的大小,並與size相加來更新已經使用的緩存大小。之後將本次提交的數據插入到LinkedHashMap裏。
在調用LinkedHashMap的put方法時,如果LinkedHashMap中已經存在以這個”Key”爲鍵的數據,則會用新插入的數據覆蓋舊的數據,然後將舊的數據返回,返回的舊數據被保存在了previous這個變量內,如果LinkedHashMap中不存在以這個”Key”爲鍵的數據則返回null。因此,當previous不爲null時,說明有舊數據被覆蓋了,我們要減去這個舊數據所佔用的空間大小。
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) {//如果當前已用緩存大小不超過最大緩存大小,則用break停止循環
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++;
}
entryRemoved(true, key, value, null);
}
}
當已用緩存大小超過最大緩存時,通過map.eldest()來獲取訪問時間最早的那個元素,然後將它從LinkedHashMap中刪除,並重新計算已用緩存的大小,如果已用緩存的大小仍然大於最大緩存大小,則進行下一次循環繼續進行刪除,否則打斷循環。
獲取緩存
LruCache對象通過get方法來獲取一個緩存。get方法的源碼如下:
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);//從LinkedHashMap中拿取緩存對象
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
V createdValue = create(key);//通過create方法試圖創建一個對象
if (createdValue == null) {
return null;
}
//以下代碼與put方法類似
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
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;
}
}
put方法的代碼略長,但理解起來其實非常簡單。依舊是宣判斷key是否爲null。之後通過map.get(key)來從LinkedHashMap中獲取緩存的數據。如果獲取的數據不爲null,則直接將其返回,並且LinkedHashMap會自動將這個緩存對象排列到最後面。
如果map.get(key)獲取的值爲空,則會試圖調用create方法來創建一個對象,create默認情況下直接返回null。我們可以根據需求重寫create方法來實現自己想要的結果。如果create成功的創建了一個對象,LruCache則會將這個對象添加到LinkedHashMap中,並返回該對象。
remove方法
remove方法用來移除一個緩存對象,源碼如下:
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;
}
在理解了put和get方法後,remove的源碼讀起來就毫無壓力了。通過map.remove(key)來直接從LinkedHashMap中移除該對象,並更新size的值,然後調用entryRemoved方法進行處理。代碼很簡單,就不再多說了。