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原始碼
- 談談LruCache原始碼原始碼
- Android 記憶體快取框架 LruCache 的原始碼分析Android記憶體快取框架原始碼
- Android Retrofit原始碼解析Android原始碼
- Android原始碼解析-LiveDataAndroid原始碼LiveData
- Android setContentView原始碼解析AndroidView原始碼
- Android Handler 原始碼解析Android原始碼
- Android 8.1 Handler 原始碼解析Android原始碼
- Android LayoutInflater Factory 原始碼解析Android原始碼
- [Android] Retrofit原始碼:流程解析Android原始碼
- Android 原始碼分析之 EventBus 的原始碼解析Android原始碼
- Android AccessibilityService機制原始碼解析Android原始碼
- Android View 原始碼解析(一) - setContentViewAndroidView原始碼
- React Native 0.55.4 Android 原始碼分析(Java層原始碼解析)React NativeAndroid原始碼Java
- Android 網路框架 Retrofit 原始碼解析Android框架原始碼
- Android系統原始碼目錄解析Android原始碼
- weex原始碼解析(四)- android引入sdk原始碼Android
- Android原始碼分析(LayoutInflater.from(this).inflate(resId,null);原始碼解析)Android原始碼Null
- Android NFC技術解析,附Demo原始碼Android原始碼
- Android OkHttp原始碼解析入門教程(一)AndroidHTTP原始碼
- Android OkHttp原始碼解析入門教程(二)AndroidHTTP原始碼
- Android進階必學retrofit原始碼解析Android原始碼
- android apk安裝過程原始碼解析AndroidAPK原始碼
- Android中的LruCache的原理和使用Android
- Android八門神器(一):OkHttp框架原始碼解析AndroidHTTP框架原始碼
- Android原始碼完全解析——View的Measure過程Android原始碼View
- Weex Android原始碼解析(三)—— 進入正題Android原始碼
- android面試——開源框架的原始碼解析Android面試框架原始碼
- Android網路程式設計:Retrofit原始碼解析Android程式設計原始碼
- Android 9.0 原始碼_機制篇 -- 全面解析 HandlerAndroid原始碼
- 容器類原始碼解析系列(一) ArrayList 原始碼分析——基於最新Android9.0原始碼原始碼Android
- Android進階:四、RxJava2 原始碼解析 1AndroidRxJava原始碼
- Android 事件分發機制原始碼解析-view層Android事件原始碼View
- Android技術棧(五)HashMap和ArrayMap原始碼解析AndroidHashMap原始碼
- Android進階:五、RxJava2原始碼解析 2AndroidRxJava原始碼
- Android構建工具--AAPT2原始碼解析(一)AndroidAPT原始碼
- LRUCache
- Android View 原始碼解析(三) – View的繪製過程AndroidView原始碼
- Flutter在Android端註冊外掛流程原始碼解析FlutterAndroid原始碼