談談LruCache原始碼
LruCache,首先從名字就可以看出它的功能。作為較為常用的快取策略,它在日常開發中起到了重要的作用。例如Glide中,它與 SoftReference 在Engine類中快取圖片,可以減少流量開銷,提升載入圖片的效率。在API12時引入android.util.LruCache,然而在API22時對它進行了修改,引入了android.support.v4.util.LruCache。我們在這裡分析的是support包裡的LruCache
什麼是LruCache演算法?
Lru(Least Recently Used),也就是最近最少使用演算法。它在內部維護了一個LinkedHashMap,在put資料的時候會判斷指定的記憶體大小是否已滿。若已滿,則會使用最近最少使用演算法進行清理。至於為什麼要使用LinkedHashMap儲存,因為LinkedHashMap內部是一個數組加雙向連結串列的形式來儲存資料,也就是說當我們通過get方法獲取資料的時候,資料會從佇列跑到隊頭來。反反覆覆,隊尾的資料自然是最少使用到的資料。

LruCache如何使用?
初始化
一般來說,我們都是取執行時最大記憶體的八分之一來作為記憶體空間,同時還要覆寫一個sizeOf的方法。特別需要強調的是,sizeOf的單位必須和記憶體空間的單位一致。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(maxMemory / 8) { @Override protected int sizeOf(@NonNull String key, @NonNull Bitmap value) { return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; 複製程式碼
API
公共方法 | |
---|---|
final int |
createCount() 返回返回值的次數 create(Object) 。 |
final void |
evictAll() 清除快取,呼叫 entryRemoved(boolean, K, V, V) 每個刪除的條目。 |
final int |
evictionCount() 返回已被驅逐的值的數量。 |
final V |
get(K key) 返回 key 快取中是否存在的值,還是可以建立的值 #create 。 |
final int |
hitCount() 返回返回 get(K) 已存在於快取中的值的次數。 |
final int |
maxSize() 對於不覆蓋的快取記憶體 sizeOf(K, V) ,這將返回快取記憶體中的最大條目數。 |
final int |
missCount() 返回 get(K) 返回null或需要建立新值的次數。 |
final V |
put(K key, V value) 快取 value 的 key 。 |
final int |
putCount() 返回 put(K, V) 呼叫的次數。 |
final V |
remove(K key) 刪除條目( key 如果存在)。 |
void |
resize(int maxSize) 設定快取的大小。 |
final int |
size() 對於不覆蓋的快取記憶體 sizeOf(K, V) ,這將返回快取記憶體中的條目數。 |
final Map<K, V> |
snapshot() 返回快取的當前內容的副本,從最近最少訪問到最近訪問的順序排序。 |
final String |
toString() |
void |
trimToSize(int maxSize) 刪除最舊的條目,直到剩餘條目的總數等於或低於請求的大小。 |
LruCache原始碼分析
我們接下里從構造方法開始為大家進行講解:
建構函式
public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } else { this.maxSize = maxSize; this.map = new LinkedHashMap(0, 0.75F, true); } } 複製程式碼
建構函式一共做了兩件事。第一節:判斷maxSize是否小於等於0。第二件,初始化maxSize和LinkedHashMap。沒什麼可說的,我們接著往下走。
safeSizeOf(測量元素大小)
private int safeSizeOf(K key, V value) { int result = this.sizeOf(key, value); if (result < 0) {//判空 throw new IllegalStateException("Negative size: " + key + "=" + value); } else { return result; } } 複製程式碼
sizeOf (測量元素大小)
這個方法一定要覆寫,否則存不進資料。
protected int sizeOf(@NonNull K key, @NonNull V value) { return 1; } 複製程式碼
put方法 (增加元素)
@Nullable public final V put(@NonNull K key, @NonNull V value) { if (key != null && value != null) { Object previous; synchronized(this) { ++this.putCount;//count為LruCahe的快取個數,這裡加一 this.size += this.safeSizeOf(key, value);//加上這個value的大小 previous = this.map.put(key, value);//存進LinkedHashMap中 if (previous != null) {//如果之前存過這個key,則減掉之前value的大小 this.size -= this.safeSizeOf(key, previous); } } if (previous != null) { this.entryRemoved(false, key, previous, value); } this.trimToSize(this.maxSize);//進行記憶體判斷 return previous; } else { throw new NullPointerException("key == null || value == null"); } } 複製程式碼
在synchronized程式碼塊裡,進入的就是一次插入操作。我們往下,俺老孫定眼一看,似乎trimToSize這個方法有什麼不尋常的地方?
trimToSize (判斷是否記憶體溢位)
public void trimToSize(int maxSize) { while(true) {//這是一個無限迴圈,目的是為了移除value直到記憶體空間不溢位 Object key; Object value; synchronized(this) { if (this.size < 0 || this.map.isEmpty() && this.size != 0) {//如果沒有分配記憶體空間,丟擲異常 throw new IllegalStateException(this.getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } if (this.size <= maxSize || this.map.isEmpty()) {//如果小於記憶體空間,just so so~ return; } //否則將使用Lru演算法進行移除 Entry<K, V> toEvict = (Entry)this.map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); this.map.remove(key); this.size -= this.safeSizeOf(key, value); ++this.evictionCount;//回收次數+1 } this.entryRemoved(true, key, value, (Object)null); } } 複製程式碼
這個TrimToSize方法的作用在於判斷記憶體空間是否溢位。利用無限迴圈,將一個一個的最少使用的資料給剔除掉。
get方法 (獲取元素)
@Nullable public final V get(@NonNull K key) { if (key == null) { throw new NullPointerException("key == null"); } else { Object mapValue; synchronized(this) { mapValue = this.map.get(key); if (mapValue != null) { ++this.hitCount;//命中次數+1,並且返回mapValue return mapValue; } ++this.missCount;//未命中次數+1 } /* 如果未命中,會嘗試利用create方法建立物件 create需要自己實現,若未實現則返回null */ V createdValue = this.create(key); if (createdValue == null) { return null; } else { synchronized(this) { //建立了新物件之後,再將其新增進map中,與之前put方法邏輯基本相同 ++this.createCount; mapValue = this.map.put(key, createdValue); if (mapValue != null) { this.map.put(key, mapValue); } else { this.size += this.safeSizeOf(key, createdValue); } } if (mapValue != null) { this.entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { this.trimToSize(this.maxSize);//每次加入資料時,都需要判斷一下是否溢位 return createdValue; } } } } 複製程式碼
create方法 (嘗試創造物件)
@Nullable protected V create(@NonNull K key) { return null;//這個方法需要自己實現 } 複製程式碼
get方法和create方法的註釋已經寫在了程式碼上,這裡邏輯同樣不是很複雜。但是我們需要注意的是map的get方法,既然LinkedHashMap能實現Lru演算法,那麼它的內部一定不簡單!
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; } 複製程式碼
LinkedHashMap中,首先進行了判斷,是否找到該元素,沒找到則返回null。找到則呼叫afterNodeAccess方法。
LinkedHashMap的afterNodeAccess方法
void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMapEntry<K,V> last; //accessOrder為true 且當前節點不是尾節點 則按訪問順序排序 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; } } 複製程式碼
原來如此!LinkedHashMap在這個方法中實現了按訪問順序排序,這也就是為什麼我們的LruCache底層是使用的LinkedHashMap作為資料結構。
主要方法已經講完了 ,接下里我們就看看其他方法吧。
remove (移除元素)
@Nullable public final V remove(@NonNull K key) { if (key == null) {//判空 throw new NullPointerException("key == null"); } else { Object previous; synchronized(this) { previous = this.map.remove(key);//根據key移除value if (previous != null) { this.size -= this.safeSizeOf(key, previous);//減掉value的大小 } } if (previous != null) { this.entryRemoved(false, key, previous, (Object)null); } return previous; } } 複製程式碼
evictAll方法(移除所有元素)
public final void evictAll() { this.trimToSize(-1);//移除掉所有的value } 複製程式碼
其他方法
public final synchronized int size() { return this.size;//當前記憶體空間的size } public final synchronized int maxSize() { return this.maxSize;//記憶體空間最大的size } public final synchronized int hitCount() { return this.hitCount;//命中個數 } public final synchronized int missCount() { return this.missCount;//未命中個數 } public final synchronized int createCount() { return this.createCount;//建立Value的個數 } public final synchronized int putCount() { return this.putCount;//put進去的個數 } public final synchronized int evictionCount() { return this.evictionCount;//移除個數 } public final synchronized Map<K, V> snapshot() { return new LinkedHashMap(this.map);//建立LinkedHashMap } public final synchronized String toString() {//toString int accesses = this.hitCount + this.missCount; int hitPercent = accesses != 0 ? 100 * this.hitCount / accesses : 0; return String.format(Locale.US, "LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", this.maxSize, this.hitCount, this.missCount, hitPercent); } 複製程式碼
最後
有了這篇文章,相信大家對LruCache的工作原理已經很清楚了吧!有什麼不對的地方希望大家能夠指正。學無止境,大家一起加油吧。