在使用Android圖片載入框架時,經常會提到三級快取,其中主要的是記憶體快取和檔案快取。 兩個快取都是用到了LruCache演算法,在Android分別對應:LruCache和DiskLruCache。
LRU演算法
作業系統中進行記憶體管理中時採用一些頁面置換演算法,如LRU、LFU和FIFO等。
其中LRU(Least recently used,最近最少使用)演算法,核心思想是當快取達到上限時,會先淘汰最近最少使用的快取。這樣可以保證快取處於一種可控的狀態,有效的防止OOM的出現。
LruCache用法
LruCache是從Android3.1開始支援,目前已經在androidx.collection支援。
初始化
LruCache初始化:
int maxCache = (int) (Runtime.getRuntime().maxMemory() / 1024);
//初始化大小:記憶體的1/8
int cacheSize = maxCache / 8;
memorySize = new LruCache<String, Bitmap>(512) {
@Override
protected int sizeOf(String key, Bitmap value) {
//重寫該方法,計算每張要快取的圖片大小
return value.getByteCount() / 1024;
}
};
複製程式碼
方法
LruCache結構圖
方法 | 描述 |
---|---|
get(K) | 通過K獲取快取 |
put(K,V) | 設定K的值為V |
remove(K) | 刪除K快取 |
evictAll() | 清除快取 |
resize(int) | 設定最大快取大小 |
snapshot() | 獲取快取內容的映象 |
LruCache實現原理
LruCache初始化只要宣告 new LruCache(maxSize)
,這個過程主要做了那些功能,看原始碼:
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
//宣告最大快取大小
this.maxSize = maxSize;
//重點,LinkedHashMap,LruCache的實現依賴
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
複製程式碼
LruCache核心思想就是淘汰最近最少使用的快取,需要維護一個快取物件列表,排列方式按照訪問順序實現。
把最近訪問過的物件放在隊頭,未訪問的物件放在隊尾,當前列表達到上限的時候,優先會淘汰隊尾的物件。
如圖(盜用下圖片):
LinkedHashMap
比較適合實現這種演算法。
LinkedHashMap
LinkedHashMap
是一個關聯陣列、雜湊表,它是執行緒不安全的,繼承自HashMap
,實現Map<K,V>
介面。
內部維護了一個雙向連結串列,在插入資料、訪問,修改資料時,會增加節點、或調整連結串列的節點順序,雙向連結串列結構可以實現插入順序或者訪問順序。
在LruCache初始化定義了this.map = new LinkedHashMap<K, V>(0, 0.75f, true)
。
LinkedHashMap
的建構函式:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
複製程式碼
布林變數accessOrder
定義了輸出的順序,true
為按照訪問順序,false
為按照插入順序。
accessOrder
設定為true
正好滿足LRU演算法的核心思想。
LruCache原始碼分析
既然LruCache底層使用LinkedHashMap,下面我們來看看它怎麼實現快取的操作的。 原始碼分析是在Android API 28版本,
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);
//使用LinkedHashMap的put方法
previous = map.put(key, value);
if (previous != null) {
//如果previous存在,減少對應快取大小
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
//檢查快取大小,刪除最近最少使用的快取
trimToSize(maxSize);
return previous;
}
複製程式碼
put方法主要是新增快取物件後,呼叫trimToSize
方法,保證快取大小,刪除最近最少使用的快取。具體的新增快取,通過LinkedHashMap
put方法實現。
LinkedHashMap
繼承自HashMap
,沒有重寫put方法,呼叫的是HashMap
的put方法,在HashMap
的putVal方法中有呼叫建立新節點的newNode
方法,LinkedHashMap
重寫了該方法。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMapEntry<K,V> p =
new LinkedHashMapEntry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
// link at the end of list
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
LinkedHashMapEntry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
複製程式碼
其中LinkedHashMapEntry定義:
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
複製程式碼
LinkedHashMapEntry
用來儲存資料,定義了before
,after
顯示上一個元素和下一個元素。
下面的操作:
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(10, 0.75f, true);
map.put(1,1);
複製程式碼
再執行map.put(2,2)
繼續執行map.put(3,3)
如果put了相同key的話,會做什麼操作。這個一起放到get方法中講解。
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++;
}
//基本不會執行到這裡,除了重寫create方法
/*
* 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;
}
}
複製程式碼
這裡主要就是呼叫LinkedHashMap
的get方法。
LinkedHashMap
的get方法:
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
複製程式碼
afterNodeAccess
方法:
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
複製程式碼
afterNodeAccess
方法會將當前被訪問的節點e,移動到內部雙向連結串列的尾部。
在put方法,map已經有三個資料。
現在操作map.get(1)
,具體的邏輯在afterNodeAccess
方法,看下每步操作後值的變化。
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
p.after = null;
...
}
複製程式碼
變數 | 值 | before | after |
---|---|---|---|
head | 1 | null | 2 |
tail | 3 | 2 | null |
last | (tail)3 | 2 | null |
p | 1 | null | null |
b | null | ||
a | 2 | 1 | 3 |
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
複製程式碼
變數 | 值 | before | after |
---|---|---|---|
head | (a)2 | null | 3 |
tail | 3 | 2 | null |
last | 3 | 2 | null |
p | 1 | null | null |
b | null | ||
a | 2 | null | 3 |
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
複製程式碼
變數 | 值 | before | after |
---|---|---|---|
head | 2 | null | 3 |
tail | 1 | 3 | null |
last | 3 | 2 | (p)1 |
p | 1 | (last)3 | null |
b | null | ||
a | 2 | null | 3 |
最後的操作結果:
trimToSize
get
和put
方法都會呼叫到
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 || map.isEmpty()) {
break;
}
Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
複製程式碼
當map
的size
大於maxSize
,會一直迴圈刪除最近最少使用的快取物件,直到快取大小小於 maxSize
。
以上就是LruCache基本原理,理解了LinkedHashMap
,可以更加輕鬆地理解LruCache原理。
DiskLruCache
內部實現也有一部分基於LinkedHashMap
。