移動設備開發中,由於移動設備(手機等)的內存有限,所以使用有效的緩存技術是必要的.android提供來一個緩存工具類LruCache,開發中我們會經常用到,下面來他是如何實現的.
在package android.util包裏面有對LruCache定義的java文件.爲了能準確的理解LruCache,我們先來看看原文的說明:
* A cache that holds strong references to a limited number of values. Each time
* a value is accessed, it is moved to the head of a queue. When a value is
* added to a full cache, the value at the end of that queue is evicted and may
* become eligible for garbage collection.
簡單翻譯:LruCache緩存數據是採用持有數據的強引用來保存一定數量的數據的.每次用到(獲取)一個數據時,這個數據就會被移動(一個保存數據的)隊列的頭部,當往這個緩存裏面加入一個新的數據時,如果這個緩存已經滿了,就會自動刪除這個緩存隊列裏面最後一個數據,這樣一來使得這個刪除的數據沒有強引用而能夠被gc回收.
從上面的翻譯,可以知道LruCache的工作原理.下面來一步一步說明他的具體實現:
(1)如何實現保存的數據是有一定順序的,並且使用過一個存在的數據,這個數據就會被移動到數據隊列的頭部.這裏採用的是LinkedHashMap.
我們知道LinkedHashMap是保存一個鍵值對數據的,並且可以維護這些數據相應的順序的.一般可以保證存儲的數據按照存入的順序或者使用的順序的.下面來看看LruCache的構造方法:
public LruCache(int maxSize) {//指定緩存數據的數量
if (maxSize <= 0) {//必須大於0
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);//創建一個LinkedHashMap,並且按照訪問數據的順序排序
}
(2)如上面的構造方法可以知道,在創建一個LruCache就需要指定其緩存數據的數量.這裏要詳細解釋一下這個緩存數據的"數量"到底是指什麼:是指緩存數據對象的個數呢,還是緩存數據所佔用的內存總量呢?
答案是:都是. 可以是緩存數據的個數,也可以使緩存數據所佔用內存總量,當然也可以是其他.到底是什麼,需要看你的LruCache如何重寫這個方法:sizeOf(K key, V value)
protected int sizeOf(K key, V value) {//子類覆蓋這個方法來計算出自己的緩存對於每一個保存的數據所佔用的量
return 1;//默認返回1,這說明:默認情況下緩存的數量就是指緩存數據的總個數(每一個數據都是1).
}
那如果我使用LruCache來保存bitmap的圖片,並且希望緩存的容量是4M那這麼做?在原文的說明中,android給來這樣一個實例:
* <p>By default, the cache size is measured in the number of entries. Override
* {@link #sizeOf} to size the cache in different units. For example, this cache
* is limited to 4MiB of bitmaps:
* <pre> {@code
* int cacheSize = 4 * 1024 * 1024; // 4MiB
* LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {//保存bitmap的LruCache,容量是4M
* protected int sizeOf(String key, Bitmap value) {
* return value.getByteCount();//計算每一個緩存的圖片所佔用內存大小
* }
* }}</pre>
(3)那麼LruCache如何,何時判斷是否緩存已經滿來,並且需要移除不常用的數據呢?
其實在LruCache裏面有一個方法:trimToSize()就是用來檢測一次當前是否已經滿,如果滿來就自動移除一個數據,一直到不滿爲止:
public void trimToSize(int maxSize) {//默認情況下傳入是上面說的最大容量的值 this.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++;//更新自動移除數據的數量(次數)
}
entryRemoved(true, key, value, null);//用來通知這個數據已經被移除,如果你需要知道一個數據何時被移除你需要從寫這個方法entryRemoved
}
}
上面的源碼中我給出了說明,很好理解.這裏要注意的是trimToSize這個方法是public的,說明其實我們自己可以調用這個方法的.這一點很重要.記住他,你會用到的.
下面的問題是:trimToSize這個方法何時調用呢?
trimToSize這個方法在LruCache裏面多個方法裏面會被調用來檢測是否已經滿了,比如在往LruCache裏面加入一個新的數據的方法put裏面,還有在通過get(K key)這個方法獲取一個數據的時候等,都會調用trimToSize來檢測一次.下了來看看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);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {//加入重複位置的數據,則移除老的數據
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);//檢測緩存的數據是否已經滿
return previous;
}
(4)細心的人在看了上面的源碼可以發現,原來對 LruCache的操作都加了synchronized來保證線程安全,是的,LruCache就是線程安全的,其他的方法也都使用來synchronized
(5)其實你應該馬上有一個疑問:如果LruCache中已經刪除了一個數據,可是現在又調用LruCache的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;
}
}
從上面的分析可以知道,我們可以從寫create方法來重新創建已經不存在的數據.這個方法默認情況是什麼也不做的,所以需要你自己做
protected V create(K key) {
return null;
}