剖析Picasso中的内存缓存机制——LruCache
眾所周知,Picasso是一個優秀的Android圖片加載庫。本篇并不討論picasso的使用,而是來談一談picasso的緩存機制。
我們知道,目前主流的圖片解決方案大部分都是三級緩存,即內存緩存、本地緩存和服務器緩存。這其中內存和本地緩存是在客戶端實現的,picasso中也使用了這種方案。
Picasso的本地緩存我們在另外的篇幅中來談,今天我們重點談談picasso的內存緩存。
在picasso源碼下我們可以看到一個類——LruCache,這就是今天的主角,picasso的內存緩存就在這里實現的。
那么,為什么叫LruCache?
LRU是一種內存管理算法,是Least Recently Used的縮寫。該算法的詳細解釋大家可以自行查閱。簡單來說就是一個有序的集合,新添加的數據處于頂部,同時如果某個數據被訪問,那么這個數據會被移至頂部;而當淘汰數據時,會從底部進行淘汰。如圖:
?
這個算法的核心思想是,如果一個數據近期被訪問,那么被再次訪問的可能性會遠高于那些很久未被訪問的數據,所以優先淘汰那些很久未被訪問的數據。
了解了LRU算法,我們回過頭再來看LruCache是如何實現這個思想的呢?
在LruCache源碼中我們可以看到使用的是一個LinkedHashMap來進行數據存儲的。LinkedHashMap是HashMap的子類,不同于HashMap之處在于它同時實現了雙向鏈表,保證了數據的插入順序,所以實際上最新插入的數據處于鏈表的尾部。而且LinkedHashMap可以用來實現LRU算法。怎么實現?我們來看看LruCache的構造函數
public?LruCache(int?maxSize)?{if?(maxSize?<=?0)?{throw?new?IllegalArgumentException("Max?size?must?be?positive.");}this.maxSize?=?maxSize;this.map?=?new?LinkedHashMap<String,?Bitmap>(0,?0.75f,?true); }可以看到初始化LinkedHashMap時使用三個參數的構造函數
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
重點在accessOrder這個參數,當為false表示按插入順序,當為true表示按訪問順序。LinkedHashMap內部通過重寫get函數和內部類Entry的recordAccess方法來實現這部分邏輯,代碼如下:
public?V?get(Object?key)?{??Entry<K,V>?e?=?(Entry<K,V>)getEntry(key);??if?(e?==?null)??return?null;??e.recordAccess(this);??return?e.value;??}??void?recordAccess(HashMap<K,V>?m)?{??LinkedHashMap<K,V>?lm?=?(LinkedHashMap<K,V>)m; ?if?(lm.accessOrder)?{??lm.modCount++; ?remove(); ?addBefore(lm.header);??}??}可以看到如果調用get方法就會調用recordAccess方法。而在recordAccess中如果accessOrder為true,會從集合中將這個元素remove并插入到鏈表尾部。而且注意在HashMap的put方法中,當插入的元素已經存在,也同樣會調用recordAccess方法,源碼如下:
public V put(K key, V value) {if (key == null)return putForNullKey(value);int hash = hash(key.hashCode());int i = indexFor(hash, table.length);for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}modCount++;//將key-value添加到table[i]處addEntry(hash, key, value, i);return null; }以上已經實現了訪問排序,那么淘汰機制又是如何的呢?
在Picasso中使用了一個閥值來限制緩存的整體大小,在上面的構造函數中我們可以看到maxSize這個參數,這個就是緩存的最大閥值。如果我們沒有自己定義這個閥值,那么picasso會使用一個默認大小,我們來看看LruCache的另外一個構造函數:
public?LruCache(Context?context)?{this(Utils.calculateMemoryCacheSize(context)); } 可以看到是通過Utils的相關函數計算出的默認閥值,通過源碼我們來看看具體怎么得到的 static?int?calculateMemoryCacheSize(Context?context)?{ActivityManager?am?=?getService(context,?ACTIVITY_SERVICE);boolean?largeHeap?=?(context.getApplicationInfo().flags?&?FLAG_LARGE_HEAP)?!=?0;int?memoryClass?=?am.getMemoryClass();if?(largeHeap?&&?SDK_INT?>=?HONEYCOMB)?{memoryClass?=?ActivityManagerHoneycomb.getLargeMemoryClass(am);}//?Target?~15%?of?the?available?heap.return?1024?*?1024?*?memoryClass?/?7; }其中memoryClass是單個應用的最大內存限制,這個數值在不同的設備上是不一樣的。
注意可以看到在HONEYCOME以上版本使用了不同的函數:getMemoryClass獲取的是系統為應用分配的內存,不包含額外的補充;但是我們可以在manifest中將LargeHeap設為true來獲取最大內存,而getLargeMemoryClass獲取的就是這個補充后的內存大小。本篇就不詳細展開了。
memoryClass的單位是MB,所以最后返回的閥值是應用最大內存的1/7,也就是說picasso的內存緩存最大會占用應用最大內存的15%左右。所以它不是一個固定值,而且因設備不同而不同的。
那么如何使用這個閥值來保證緩存空間大小的呢?我們來看看LruCache的set函數:
@Override?public?void?set(String?key,?Bitmap?bitmap)?{if?(key?==?null?||?bitmap?==?null)?{throw?new?NullPointerException("key?==?null?||?bitmap?==?null");}Bitmap?previous;synchronized?(this)?{putCount++;size?+=?Utils.getBitmapBytes(bitmap);previous?=?map.put(key,?bitmap);if?(previous?!=?null)?{size?-=?Utils.getBitmapBytes(previous);}}trimToSize(maxSize); }可以看到當set一個數據時,會增加緩存的當前大小,通過Utils.getBitmapBytes函數來獲取圖片大小并加至size參數上。
這里可以注意到,如果是已經存在的元素,則增加大小后還需要減去舊圖片的大小,保證size的正確性。
最后會調用trimToSize函數,這個函數源碼如下:
private?void?trimToSize(int?maxSize)?{while?(true)?{String?key;Bitmap?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<String,?Bitmap>?toEvict?=?map.entrySet().iterator().next();key?=?toEvict.getKey();value?=?toEvict.getValue();map.remove(key);size?-=?Utils.getBitmapBytes(value);evictionCount++;}} }當緩存大小超過閥值,開始從頭遍歷map,清除圖片緩存并改變緩存大小,直到緩存大小不再超過閥值。
因為在map中尾部的數據是最新的,而頭部是最久未訪問的,這樣便實現了淘汰機制。
綜上,我們剖析了picasso的內存緩存機制,它是在LinkedHashMap的基礎上實現了LRU算法來進行內存管理,并且根據系統默認了一個合理的閥值,當緩存過大時會先清除久未使用的緩存。這樣既加速了圖片的加載,同時也保證了不過分消耗應用的內存。
這里要提一下Facebook開源的Fresco,同樣作為圖片加載解決方案,但是fresco使用了另外一種內存緩存——匿名共享內存。匿名共享內存實際上是在c/c++層創建使用的,所以不受Android虛擬機的內存限制,同時也不占用應用的最大內存。由于不少本篇內容,就簡單介紹一下,有時間我會專門整理一篇文章來細說。
?
總結
以上是生活随笔為你收集整理的剖析Picasso中的内存缓存机制——LruCache的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决Picasso在Android 5.
- 下一篇: 解决listview中的textview