Android 緩衝類LruCache源碼分析
1.引言
Android開發難免會遇到載入網狀圖片問題,而載入網狀圖片難免會遇到多次重複請求載入同一張圖片問題,這麼一來就導致多次載入網路,不僅浪費資源而且給使用者體驗感覺載入圖片慢。從Android3.1開始,API中多了一個LruCache類,該類是一個使用最近最少使用演算法的緩衝類。也就是可以把上次下載的圖片以索引值對的方式儲存在緩衝中,以便下載載入同一張圖片時無須再次從網路上下載,而且載入速度快。
2.LruCache源碼詳解
這裡我把LruCache源碼貼出來,幾乎每行代碼的注釋都有,很詳細,有意者可以仔細跟著注釋閱讀源碼,以便理解LruCache類的原理。
/**1.從類名LruCache就知道,該類的作用是一個最近最少使用演算法來維護的一個緩衝類。2.該類是一個用於緩衝一定數量的值,並且該緩衝對象是持有強引用。3.每當成功get一次值時,該值都會移動到鏈表的頭部,以便標記該值為最近最新的值。4.當緩衝滿了時,鏈表尾部的值會被認為是最近最少使用的值,會被從鏈表中移除,以便緩衝有空間儲存新的值。5.緩衝中鏈表的值發生改變時會調用空方法entryRemoved,開發人員可以重寫該方法,以便做相應的操作。6.開發人員應該去重寫該類中sizeOf方法,該方法返回每個key對應value值得大小,以便該類去維護緩衝的大小。7.該類是一個安全類。同時該類不允許key和value為空白。該類在Android3.1之後添加到源碼中。**/package android.util;import java.util.LinkedHashMap;import java.util.Map;public class LruCache { private final LinkedHashMap map; /** Size of this cache in units. Not necessarily the number of elements. */ private int size;//已使用緩衝大小 private int maxSize;//總的緩衝大小 private int putCount;//添加記錄次數 private int createCount;//建立次數 private int evictionCount;//移除次數 private int hitCount;//叫用次數 private int missCount;//未叫用次數 /** * @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(0, 0.75f, true);//建立鏈表緩衝,該鏈表緩衝是整個LruCache類的重點。 } /** * Sets the size of the cache. * @param maxSize The new maximum size. *重新設定緩衝大小 * @hide */ public void resize(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } synchronized (this) { this.maxSize = maxSize; } trimToSize(maxSize); } /** * Returns the value for {@code key} if it exists in the cache or can be * created by {@code #create}. If a value was returned, it is moved to the * head of the queue. This returns null if a value is not cached and cannot * be created. * 根據key值獲得緩衝中對應的資料,該方法是安全執行緒的。 */ 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++;//標記未命中的次數 } /* * 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);//建立新的資料,預設返回null,建立的時候可以重寫該方法,以便未命中的時候建立一個新的資料添加到緩衝鏈表中。 if (createdValue == null) { return null;//未命中時預設到這裡結束。 } //以下代碼是在未命中時建立新資料添加到鏈表緩衝中。 synchronized (this) { createCount++;//標記建立新資料的次數 mapValue = map.put(key, createdValue); //如果鏈表中已有該key對應的值,則最後取消添加新建立的值。很多人可能感到奇怪,此時鏈表中應該不存在key對應的值啊?其實這裡是為了防止多線程操作導致資料不同步而添加的安全的程式碼。不懂得自己體會去吧! if (mapValue != null) { // There was a conflict so undo that last put map.put(key, mapValue); } else { //標記已經使用了的記憶體大小。 size += safeSizeOf(key, createdValue); } } if (mapValue != null) { //鏈表中發生資料變化時(調用了put,get方法)調用該方法。該方法預設是個空,開發人員可以重寫該方法在鏈表中資料變化時做相應的操作。 entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { //重新整理當前鏈表維護的記憶體。 trimToSize(maxSize); return createdValue; } } /** * 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) { putCount++;//標記添加資料的次數 size += safeSizeOf(key, value);//標記已使用的記憶體大小 previous = map.put(key, value);//儲存資料到鏈表 if (previous != null) {//鏈表中已存在key對應的值則替換原來的值 size -= safeSizeOf(key, previous);//標記已使用的記憶體大小 } } if (previous != null) { //鏈表中資料發生交換時調用該方法。 entryRemoved(false, key, previous, value); } //整理鏈表維護的記憶體大小 trimToSize(maxSize); return previous; } /** * @param maxSize the maximum size of the cache before returning. May be -1 * to evict even 0-sized elements. * 管理鏈表中記憶體的大小 */ private 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; } // BEGIN LAYOUTLIB CHANGE // get the last item in the linked list. // This is not efficient, the goal here is to minimize the changes // compared to the platform version. Map.Entry toEvict = null; //for迴圈得到鏈表末尾的資料,然後移除它。 for (Map.Entry entry : map.entrySet()) { toEvict = entry; } // END LAYOUTLIB CHANGE if (toEvict == null) { break; } //移除鏈表末尾的一個資料,該資料就是最近最少使用到的資料。 key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key);//移除資料 size -= safeSizeOf(key, value);//重寫計算已使用的記憶體大小 evictionCount++;//標記移除資料的次數 } //開發人員可以重新改方法,當移除資料時做相應的操作。 entryRemoved(true, key, value, null); } } /** * Removes the entry for {@code key} if it exists. * * @return the previous value mapped by {@code key}. * 從鏈表中移除資料的方法 */ public final V remove(K key) { if (key == null) { throw new NullPointerException("key == null"); } V previous; synchronized (this) { previous = map.remove(key); if (previous != null) { size -= safeSizeOf(key, previous); } } if (previous != null) { entryRemoved(false, key, previous, null); } return previous; } /** * Called for entries that have been evicted or removed. This method is * invoked when a value is evicted to make space, removed by a call to * {@link #remove}, or replaced by a call to {@link #put}. The default * implementation does nothing. * *The method is called without synchronization: other threads may * access the cache while this method is executing. * * @param evicted true if the entry is being removed to make space, false * if the removal was caused by a {@link #put} or {@link #remove}. * @param newValue the new value for {@code key}, if it exists. If non-null, * this removal was caused by a {@link #put}. Otherwise it was caused by * an eviction or a {@link #remove}. */ //該方法預設是一個空方法。當鏈表中發生資料變化時調用該方法,所以開發人員可以重寫該方法,以便做相應的操作。 protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} /** * Called after a cache miss to compute a value for the corresponding key. * Returns the computed value or null if no value can be computed. The * default implementation returns null. * *
The method is called without synchronization: other threads may * access the cache while this method is executing. * *
If a value for {@code key} exists in the cache when this method * returns, the created value will be released with {@link #entryRemoved} * and discarded. This can occur when multiple threads request the same key * at the same time (causing multiple values to be created), or when one * thread calls {@link #put} while another is creating a value for the same * key. */ //該方法預設返回空,開發人員可以重新該方法以便在get鏈表中的資料失敗時建立新的資料添加到鏈表中。 protected V create(K key) { return null; } //該方法是重寫計算鏈表中已經使用了的記憶體大小 private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; } /** * Returns the size of the entry for {@code key} and {@code value} in * user-defined units. The default implementation returns 1 so that size * is the number of entries and max size is the maximum number of entries. * *
An entry's size must not change while it is in the cache. */ //該方法預設返回1,開發人員必須重寫該方法,用來計算每個key對應value值得大小,以便維護鏈表中緩衝的大小。 protected int sizeOf(K key, V value) { return 1; } /** * Clear the cache, calling {@link #entryRemoved} on each removed entry. */ //移除鏈表中所有的資料 public final void evictAll() { trimToSize(-1); // -1 will evict 0-sized elements } /** * For caches that do not override {@link #sizeOf}, this returns the number * of entries in the cache. For all other caches, this returns the sum of * the sizes of the entries in this cache. * 得到已經使用緩衝中記憶體的大小 */ public synchronized final int size() { return size; } /** * For caches that do not override {@link #sizeOf}, this returns the maximum * number of entries in the cache. For all other caches, this returns the * maximum sum of the sizes of the entries in this cache. * 得到分配給鏈表緩衝的大小 */ public synchronized final int maxSize() { return maxSize; } /** * Returns the number of times {@link #get} returned a value that was * already present in the cache. * 得到命中的次數 */ public synchronized final int hitCount() { return hitCount; } /** * Returns the number of times {@link #get} returned null or required a new * value to be created. * 得到未命中的次數 */ public synchronized final int missCount() { return missCount; } /** * Returns the number of times {@link #create(Object)} returned a value. * 得到建立新資料的次數 */ public synchronized final int createCount() { return createCount; } /** * Returns the number of times {@link #put} was called. * 得到添加新資料次數 */ public synchronized final int putCount() { return putCount; } /** * Returns the number of values that have been evicted. * 得到移除資料的次數 */ public synchronized final int evictionCount() { return evictionCount; } /** * Returns a copy of the current contents of the cache, ordered from least * recently accessed to most recently accessed. * 得到鏈表對象 */ public synchronized final Map snapshot() { return new LinkedHashMap(map); } @Override public synchronized final String toString() { int accesses = hitCount + missCount; int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", maxSize, hitCount, missCount, hitPercent); } }
LruCache圖片使用樣本
一下代碼是圖片緩衝類ImageCache範例程式碼:
import android.graphics.Bitmap;import android.util.LruCache;/** * Created by xjp on 2016/4/2. */public class ImageCache { private LruCache mLruCache; private static ImageCache instance = new ImageCache(); public ImageCache() { //得到當前應用總的記憶體大小 int appTotalCache = (int) Runtime.getRuntime().totalMemory(); //取當前應用記憶體的1/8作為緩衝大小 int maxSize = appTotalCache / 8; mLruCache = new LruCache(maxSize) { @Override protected int sizeOf(String key, Bitmap value) { //此處返回每個Bitmap對象的大小。值得注意:此處傳回值並不是圖片的張數, // 且傳回值的單位應該和maxSize的單位一樣。也就是maxSize單位是B,那麼此處傳回值單位也是B return value.getByteCount(); } @Override protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { //如果緩衝中的值被移除,則去回收Bitmap記憶體。 if (evicted) { if (oldValue != null && !oldValue.isRecycled()) { oldValue.recycle(); } } } }; } public static ImageCache getInstance() { return instance; } //從緩衝中讀取資料 public Bitmap getBitmapFromCache(String key) { if (mLruCache != null) { mLruCache.get(key); } return null; } //儲存資料到緩衝中 public void putBitmapToCache(String key, Bitmap value) { if (mLruCache != null) { if (mLruCache.get(key) == null) { mLruCache.put(key, value); } } } //清除緩衝中所有資料 public void clearCache() { if (mLruCache != null) { mLruCache.evictAll(); } }}
總結:以後就可以直接用ImageCache類來作為圖片緩衝了。