12.圖片三級快取和LruCache原始碼
大多的開源圖片框架針對圖片載入都採用了三級快取的方式,大概流程通常是這樣的,載入圖片時,首先檢查記憶體中是否仍然保有這個圖片物件,如果有則直接顯示到控制元件上,載入過程到此結束;如果記憶體中沒有,則可能是第一次載入,還沒有快取或者記憶體中的快取被銷燬,這時候去本地快取中讀取,通常是寫入到了檔案中,如果檔案中讀取到了快取,則設定給控制元件顯示,載入結束,如果沒有快取,則再請求伺服器返回,這時候會將獲取到的圖片寫入到本地硬碟(檔案)中,(或者同時在記憶體中也寫入一份),同時設定給圖片顯示。這時第一次載入結束,下次再次載入時,重複上一個過程,只要存在快取就不再請求網路,降低不必要的網路請求。如下圖

圖片三級快取.png
記憶體快取是如何實現的,我們當然可以用一個HashMap來儲存獲取到的bitmap,以url的md5值為key來儲存,但是有一個問題需要注意,安卓系統為每一個應用分配的記憶體都是有限的,使用HashMap固然可以實現功能,但是當圖片足夠多的時候,HashMap無法為你清理記憶體,極有可能發生記憶體溢位。
為了防止這種問題,可以在把bitmap加如到集合中時,使用軟引用,弱引用,虛引用等包裹bitmap,這樣可以防止記憶體溢位,及時的清理bitmap,但有一個問題,這樣記憶體快取的作用就不存在了,我們的目的是做快取,但從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的物件,軟引用和弱引用已經不再可靠了,試想如果你剛剛加入快取就被系統清理了,達不到我們想要的效果,所以Android系統提供了一個可靠的快取集合LruCache,LruCache內部封裝了一個LinkedHashMap集合,所以也可以把它當作集合來看待
long maxMemory = Runtime.getRuntime().maxMemory();//獲取Dalvik 虛擬機器最大的記憶體大小:16 LruCache<String, Bitmap> lruCache = new LruCache<String,Bitmap>((int) (maxMemory/8)){//指定記憶體快取集合的大小 //獲取圖片的大小 @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes()*value.getHeight(); } };
LruCache建立的時候通過泛型指定map集合的key和value型別,並且通過構造方法傳入設定的記憶體快取集合的最大值,我們來看看它的構造方法,可以看到,這裡儲存了記憶體的最大佔用空間,並且建立了一個LinkedHashMap,相比於HashMap,保證有序性
/** * @param maxSize for caches that do not override {@link #sizeOf}, this is *the maximum number of entries in the cache. For all other caches, *this is the maximum sum of the sizes of the entries in this cache. */ 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中新增內容的時候,進入put方法
/** * Caches {@code value} for {@code key}. The value is moved to the head of * the queue. * * @return the previous value mapped by {@code key}. */ public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { //每加如一個快取物件,快取計數器增加1 putCount++; //計算出當前快取的大小值,safeSizeOf就是上邊重寫的sizeOf //方法的封裝,得到的是當前加如的快取物件的大小,然後累加得到總大小 size += safeSizeOf(key, value); //如果快取中存在相同的物件,總的快取大小減去當前這個存入的大小 //也就是重複的快取物件不計入快取,map集合在put的時候,如果集合中存在的 //話會用新的value值替換舊的value,不存在重複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
每次向LruCache中新增新的物件快取時,都會檢查一次當前快取大小是否超過了設定的最大值,這是一個死迴圈,只要佔用的空間大小極值,就會一直根據lru演算法來得到最符合移除條件的一個物件然後移除它,直到記憶體大小在合理範圍內
/** * Remove the eldest entries until the total of remaining entries is at or * below the requested size. * * @param maxSize the maximum size of the cache before returning. May be -1 *to evict even 0-sized elements. */ 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; } //根據lru演算法(Least recently used,最近最少使用)得到一個entry 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); } }
看到這裡可以知道,其實LruCache的關鍵在於LinkedHashMap內部是如何運轉的,它會根據lru演算法,獲取到最符合移除條件的一個物件,eldest()方法返回的就是我們需要的值,那麼究竟是如何判斷的呢,只有進入原始碼去看一下了
在LruCache例項化的時候,我們看到LinkedHashMap是這樣構建的,關鍵在於最後一個true的標記,這個值代表什麼?
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
LinkedHashMap有五種構造方法,前四個構造方法都將accessOrder設為false,預設是按照插入順序排序的;而第五個構造方法可以自定義傳入的accessOrder的值,因此可以指定雙向迴圈連結串列中元素的排序規則。特別地,當我們要用LinkedHashMap實現LRU演算法時,就需要呼叫該構造方法並將accessOrder置為true。本質上,LinkedHashMap = HashMap + 雙向連結串列,可以這樣理解,LinkedHashMap 在不對HashMap做任何改變的基礎上,給HashMap的任意兩個節點間加了兩條連線(before指標和after指標),使這些節點形成一個雙向連結串列。在LinkedHashMapMap中,所有put進來的Entry都儲存在HashMap中,並且對於每次put進來Entry還會將其插入到雙向連結串列的尾部。
我們看看設定為true或者false,連結串列的存取有什麼區別,找到put方法,LinkedHashMap的put方法使用的是父類HashMap的put
public V put(K key, V value) { return putVal(hash(key), key, value, false, true) } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //這裡可以看到,當存在相同的key時,會將新的value替換舊的value,並將舊的返回 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //走到了這個方法,但其實這個方法在HashMap中是空的,LinkedHashMap重寫了這個 //方法,我們看看它是如何實現 afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
可以看到,LinkedHashMap呼叫put方法新增資料的時候,除了繼承了HashMap把資料新增到hash表中,還做了另一步操作,就是同時加入了它內部的一個雙向連結串列中,並且是加入了尾部,如果是第一個加如得資料自然也就是頭部了,那麼這時差不多明白了,LinkedHashMap方法得eldest方法返回得值怎麼就是最近最少使用得呢
void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMapEntry<K,V> last; //這裡是構造LinkedHashMap時傳入的標記位,只有為true的情況才會加如連結串列, //也就是說只有第五種構造方法被呼叫並且accessOrder 賦值為true,連結串列才會起作用 //同時判斷一個條件,當前要加如連結串列的value和當前連結串列中最後一個value是否相同 //只有不同的情況才會將其加入 if (accessOrder && (last = tail) != e) { //注意這種結構,p b a都是LinkedHashMapEntry型別,這一行程式碼寫的很簡潔 //第一,將e強轉為LinkedHashMapEntry p,第二將p在連結串列中的位置放在b的後邊 //(b = p.before),將p的後邊指定為a,這裡可以猜想b就是上一次放入連結串列中的 //LinkedHashMapEntry 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; } }
上邊是存入得時候,那麼取出得時候呢
/** * Returns the value to which the specified key is mapped, * or {@code null} if this map contains no mapping for the key. * * <p>More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}.(There can be at most one such mapping.) * * <p>A return value of {@code null} does not <i>necessarily</i> * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. */ 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存取資料都會呼叫afterNodeAccess方法,將最近使用得value放在連結串列得末尾,那麼這樣一來就很清楚了,使用最多得一直放在連結串列得末尾,使用最少得自然就放在頭部了,那麼eldest方法返回得head自然也就是最滿足移除條件得最少使用得value了。
LinkedHashMap重寫了HashMap中的afterNodeAccess方法(HashMap中該方法為空),當呼叫父類的put方法時,在發現key已經存在時,會呼叫該方法;當呼叫自己的get方法時,也會呼叫到該方法。該方法提供了LRU演算法的實現,它將最近使用的Entry放到雙向迴圈連結串列的尾部。也就是說,當accessOrder為true時,get方法和put方法都會呼叫recordAccess方法使得最近使用的Entry移到雙向連結串列的末尾。
此時可以回到最初得位置了,LruCache中通過這一行程式碼取得要移除得物件,從而保證將記憶體控制在合理範圍內。最根本的實現在於LinkedHashMap。
Map.Entry<K, V> toEvict = map.eldest();