LruCache源碼解析
LruCache是Android中的一個緩存工具類送巡,它采用了一種最近最少使用算法芦圾,可以將一些對象進行內(nèi)存緩存,當緩存滿后婴洼,會優(yōu)先刪除近期最少使用的對象兜看。LruCache在實際開發(fā)中是使用率非常高的一個工具類锥咸,許多著名的圖片加載,網(wǎng)絡請求等框架內(nèi)部都是使用的LruCache對象進行數(shù)據(jù)緩存细移,因此我們有必要了解LruCache內(nèi)部的工作原理搏予。
基本使用
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參數(shù)速缨,這個參數(shù)代表可以緩存的最大值。而sizeOf方法用來計算要緩存的對象的大小代乃。
如果我們想要緩存一個對象旬牲,只需要調(diào)用LruCache對象的put(K key, V value)方法即可,而當我們想要拿取一個緩存時,則需要調(diào)用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參數(shù)保存在了一個成員變量中,然后初始化了一個LinkedhashMap對象擂橘。這個LinkedHashMap其實就是LruCache的核心部分晌区,使用LruCache緩存的對象其實是存儲在這個LinkedHashMap中的。
LinkedHashMap是HashMap的一個子類贝室,與HashMap不同的是契讲,LinkedHashMap能夠記住每條記錄的插入順序仿吞。LinkedHashMap類有許多重載的構造方法滑频,而在LruCache中使用的是LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)這個構造方法,其中最關鍵的就是accessOrder這個參數(shù)唤冈,它代表著LinkedhashMap中每條記錄的排序規(guī)則峡迷。當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);
//采用這個無參的構造方法創(chuàng)建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就是根據(jù)LinkedHashMap這個特性來判斷哪些緩存數(shù)據(jù)需要被優(yōu)先清除的卒暂。
插入緩存
LruCache使用put方法來插入一個緩存數(shù)據(jù)啄栓,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);//更新已經(jīng)使用的緩存的大小
previous = map.put(key, value); //將數(shù)據(jù)插入到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變量代表當前已經(jīng)使用的緩存的大小诈嘿,使用sizeOf方法來計算本次提交的緩存數(shù)據(jù)的大小堪旧,并與size相加來更新已經(jīng)使用的緩存大小。之后將本次提交的數(shù)據(jù)插入到LinkedHashMap里奖亚。
在調(diào)用LinkedHashMap的put方法時崎场,如果LinkedHashMap中已經(jīng)存在以這個"Key"為鍵的數(shù)據(jù),則會用新插入的數(shù)據(jù)覆蓋舊的數(shù)據(jù)遂蛀,然后將舊的數(shù)據(jù)返回谭跨,返回的舊數(shù)據(jù)被保存在了previous這個變量內(nèi),如果LinkedHashMap中不存在以這個"Key"為鍵的數(shù)據(jù)則返回null。因此螃宙,當previous不為null時蛮瞄,說明有舊數(shù)據(jù)被覆蓋了,我們要減去這個舊數(shù)據(jù)所占用的空間大小谆扎。
entryRemoved方法會在某個緩存數(shù)據(jù)被移除時被調(diào)用挂捅,默認情況下該方法是個空方法。我們可以根據(jù)需求來重寫這個方法堂湖,在緩存對象要被清除時做一些處理闲先。
最后調(diào)用了trimToSize方法來計算當前緩存數(shù)據(jù)的大小是否超過了緩存允許的最大值的限制,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停止循環(huán)
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中刪除斥季,并重新計算已用緩存的大小训桶,如果已用緩存的大小仍然大于最大緩存大小,則進行下一次循環(huán)繼續(xù)進行刪除酣倾,否則打斷循環(huán)舵揭。
獲取緩存
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方法試圖創(chuàng)建一個對象
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中獲取緩存的數(shù)據(jù)映之。如果獲取的數(shù)據(jù)不為null拦焚,則直接將其返回,并且LinkedHashMap會自動將這個緩存對象排列到最后面惕医。
如果map.get(key)獲取的值為空耕漱,則會試圖調(diào)用create方法來創(chuàng)建一個對象,create默認情況下直接返回null抬伺。我們可以根據(jù)需求重寫create方法來實現(xiàn)自己想要的結果螟够。如果create成功的創(chuàng)建了一個對象,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的值拉鹃,然后調(diào)用entryRemoved方法進行處理辈赋。代碼很簡單鲫忍,就不再多說了。