android LruCache原始碼解析
#LruCache
LRU為最近最少使用演算法,LruCache顧名思義即為最近最少使用演算法下的快取機制。
LRU的目的是為了加快最近經常使用的資料從記憶體中取出的速度。
在android中LruCache在圖片快取中頻繁使用到,瞭解它絕對是必要的。
#Lru演算法實現
首先當然是看 LruCache這個類的原始碼,我們很容易發現LruCache類中僅僅是get,put等供開發者使用的方法,並未涉及連結串列等結構。(由於程式碼較長,筆者此處就不附上LruCache類的程式碼了)
但是其中卻又一個我們不常見的類,即LinkedHashMap,經過檢視,得出結論:
LruCache的底層是通過LinkedHashMap實現資料快取的。
LinkedHashMap是一個雙向連結串列,繼承了HashMap。
對HashMap不瞭解的可以看這篇文章:
http://blog.csdn.net/double2hao/article/details/53411594
根據LRU演算法的思路可知,LRU演算法定然是會在取出資料時對連結串列進行操作,從而加快下一次取出“經常使用的資料”的速度。
於是,我們定然先檢視get方法。
@Override public V get(Object key) {
/*
* This method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
return null;
}
除去key等於null等意外的情況不看,我們可以發現,最關鍵的其實是這幾行程式碼:
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
e即為我們要取出的那個結點,而makeTail在取出這個結點之前對其進行了操作,那麼makeTail定然便是實現LRU的方法。
接下來我們跳轉到makeTail這個方法中來。
private void makeTail(LinkedEntry<K, V> e) {
// Unlink e
e.prv.nxt = e.nxt;
e.nxt.prv = e.prv;
// Relink e as tail
LinkedEntry<K, V> header = this.header;
LinkedEntry<K, V> oldTail = header.prv;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}
根據程式碼我們可知:把e這個結點從原連結串列中取出,放到連結串列的頭結點。讓它成為原連結串列的前一個結點,成為原連結串列的尾結點的最後一個結點。(原連結串列為雙向連結串列)
所以連結串列中經常使用的結點都會被排在前面,而不經常使用的結點都會被排在後面。
這時候我們再來看get中的關鍵程式碼:
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}
}
我們可以發現,get中查詢結點的方式是從前向後找的。
因此經常使用的結點查詢時間較短,而不經常使用的結點查詢時間較長。完全符合LRU演算法的最終目的。
#如何回收不經常使用的記憶體
這時候我們似乎對LruCache是如何運作的已經有了一個瞭解了,但是心中似乎還有一個梗:
記憶體是有限的,當記憶體滿了的時候,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);
}
我們發現傳進來的記憶體LruCache中用maxSize 這個屬性進行儲存。那麼我們只要找到maxSize 具體使用的地方便可以了。
於是我們可以發現在resize和put方法中都有使用到,並且都是把maxSize 傳入了trimToSize()這個方法中使用的。
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
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;
}
於是我們可知,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;
}
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是一個LinkedHashMap物件,為了方便後面的分析,我們放上它的eldest方法程式碼,其實就是取出最久未使用的那個結點。
public Entry<K, V> eldest() {
LinkedEntry<K, V> eldest = header.nxt;
return eldest != header ? eldest : null;
}
trimToSize()的記憶體回收邏輯主要是以下幾行程式碼:
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
即查詢到最久未使用的結點並remove。
那麼這個邏輯究竟在什麼情況下會執行呢?這段程式碼之前的兩個if給了我們很好的解釋:
1、記憶體中有資料
2、記憶體中的資料超過了maxSize即提供的最大記憶體量。
至於trimToSize()的while和synchronized 其實都比較好理解
while是為了讓記憶體一直低於maxSize即最大記憶體。
synchronized 保證了執行緒安全。
相關文章
- Android——LruCache原始碼解析Android原始碼
- Android記憶體快取LruCache原始碼解析Android記憶體快取原始碼
- Java&Android 基礎知識梳理(9) - LruCache 原始碼解析JavaAndroid原始碼
- LruCache原始碼分析原始碼
- 談談LruCache原始碼原始碼
- RocksDB. LRUCache原始碼分析原始碼
- Android開源框架原始碼鑑賞:LruCache與DiskLruCacheAndroid框架原始碼
- Android 記憶體快取框架 LruCache 的原始碼分析Android記憶體快取框架原始碼
- Android Handler 原始碼解析Android原始碼
- Android Retrofit原始碼解析Android原始碼
- Android EventBus原始碼解析Android原始碼
- android原始碼解析--switchAndroid原始碼
- Android原始碼解析--LooperAndroid原始碼OOP
- Android 開源專案原始碼解析 -->PhotoView 原始碼解析(七)Android原始碼View
- Android 原始碼分析之 EventBus 的原始碼解析Android原始碼
- Android原始碼解析-LiveDataAndroid原始碼LiveData
- [Android] Retrofit原始碼:流程解析Android原始碼
- Android 8.1 Handler 原始碼解析Android原始碼
- Android LayoutInflater 原始碼解析Android原始碼
- WebRTC-Android原始碼解析WebAndroid原始碼
- android原始碼解析--DialogAndroid原始碼
- android原始碼解析--MessageQueueAndroid原始碼
- android原始碼解析--MessageAndroid原始碼
- Android fragment原始碼全解析AndroidFragment原始碼
- android原始碼解析--ListView(上)Android原始碼View
- Android 開源專案原始碼解析 -->Volley 原始碼解析(十五)Android原始碼
- Android 開源專案原始碼解析 -->Dagger 原始碼解析(十三)Android原始碼
- Android 開源專案原始碼解析 -->CircularFloatingActionMenu 原始碼解析(八)Android原始碼
- Android LayoutInflater Factory 原始碼解析Android原始碼
- Android setContentView原始碼解析AndroidView原始碼
- Android Volley框架原始碼解析Android框架原始碼
- Android系統原始碼目錄解析Android原始碼
- Android 網路框架 Retrofit 原始碼解析Android框架原始碼
- Android AccessibilityService機制原始碼解析Android原始碼
- weex原始碼解析(四)- android引入sdk原始碼Android
- Android原始碼解析(二)動畫篇-- ObjectAnimatorAndroid原始碼動畫Object
- Android開源庫——EventBus原始碼解析Android原始碼
- Android 原始碼解析 之 setContentViewAndroid原始碼View