天天看點

Android LruCache源碼詳解

尊重原創,轉載請标明出處    http://blog.csdn.net/abcdef314159

之前的兩篇我們詳細分析了HashMap和LinkedHashMap,就是為了講解LruCache做鋪墊的,這一篇我們來分析一下Android中常用的緩存類LruCache,我們知道Android中的優化比較多,其中就有一個關于圖檔緩存的問題,如果處理不好很有可能會出現ANR。在講解之前我們最好先看一下這個類的注釋,由于比較多,我隻貼出一部分

* <pre>   {@code
 *   int cacheSize = 4 * 1024 * 1024; // 4MiB
 *   LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
 *       protected int sizeOf(String key, Bitmap value) {
 *           return value.getByteCount();
 *       }
 *   }}</pre>
           

他初始化了一個4M大小的空間,一般情況下我們是使用最大記憶體的1/4或1/8,如果像上面那樣寫也是可以的。

(int)Runtime.getRuntime().maxMemory()/4
           

但要記住必須要重寫sizeOf方法,因為它預設是傳回1的,

/**
     * 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.
     *
     * <p>An entry's size must not change while it is in the cache.
     */
    protected int sizeOf(K key, V value) {
        return 1;
    }
           

如果是Bitmap的話我們就傳回Bitmap的大小。我們來看一下LruCache的構造方法

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);
    }
           

我們看到裡面封裝的是LinkedHashMap,最後一個參數是true,說明他的雙向環形連結清單是按照通路順序來存儲的。在上一篇《Android LinkedHashMap源碼詳解》中講到accessOrder參數的時候有提到過,我們來看一下他的一些方法,我們首先看一下public void trimToSize(int maxSize)這個方法

/**
     * 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;
                }

                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);
        }
    }
           

這個方法其實就是按照LRU算法移除最先加入的元素,也就是最少通路的老資料,直到他的size小于maxSize為止,我們看到18-20行,如果小于直接退出循環,22-25行,如果為空說明是map為null,直接退出循環。eldest()方法得到的是最老的,也是通路量最少的元素,是以根據LRU算法是最先移除的。剛開始看的時候一直不了解,感覺22-25行純屬多餘,因為正常的邏輯是maxSize一般是大于0的,如果18-20行成立的話,那麼22-25行肯定成立,後來看到上面的注釋才明白,maxSize如果為-1,則移除全部。我們看到第29行移除,然後30行再計算一下剩餘的size,然後在通過不斷的循環執行到上面第19行,直到size小于maxSize才終止循環。移除的時候調用的是HashMap的remove方法,并且在remove内部調用了postRemove方法,這個方法被LinkedHashMap覆寫了,移除的同時也把他從雙向環形連結清單中删除了,我們來看一下safeSizeOf方法,其實就是調用的上面sizeOf方法

private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }
           

移除的最後調用entryRemoved方法,這個方法是空方法,我們也可以在這裡實作二級緩存。接着我們來看一下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
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            if (previous != null) {
	    //我們可以看一下put方法的傳回值,如果previous為null則存進去的位置之前是空的,
	    //如果previous不為null,則存進去的位置之前是有資料的,然後把他給替換了,是以
	    //這裡要減去被替換掉的size
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
	//如果previous不為null,則表示之前的被移除了,調用entryRemoved方法
            entryRemoved(false, key, previous, value);
        }
	
	//重新計算size,保證最大size不能超過maxSize。
        trimToSize(maxSize);
        return previous;
    }
           

這個方法比較簡單,我們再來看一下另外一個方法public final V remove(K 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的,隻需要減,如果移除成功,則減去移除的size  
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
	    //如果移除成功,則調用entryRemoved方法  
            entryRemoved(false, key, previous, null);
        }

        return previous;
    }
           

這個方法和上面的put差不多,我們在看另外一個方法public final V get(K 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.
         */

	//這個方法是建立一個value,預設是傳回為null,如果沒有覆寫則傳回null直接return
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
	    //這段代碼當時一直搞不明白,如果上面的map.get(key)傳回為null的話,就表示
	    //key所對應的value是不存在的,那麼下面的map.put(key, createdValue)方法就
	    //肯定傳回為null,下面也就沒必要在進行判斷,當我在重新看protected V create(K key)
	    //方法注釋的時候,發現有這樣一段描述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 samekey.意思就是如果
	    //多線程操作時可能會引起多個value被建立
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                // There was a conflict so undo that last put
		//如果之前位置上已經有元素了,就還把原來的放回去,等于size沒變
                map.put(key, mapValue);
            } else {
		//如果之前的位置上沒有元素,說明createdValue是新加上去的,是以要加上createdValue的size
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
	//如果之前的位置上已經有元素了,就調用entryRemoved方法,createdValue表示老的,沒有存進去的,類似于
	// 删除的,
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
	//如果存進去之後要重新計算size的,如果大于maxSize要把最老的移除。
            trimToSize(maxSize);
            return createdValue;
        }
    }
           

還有最後一個方法entryRemoved

//entryRemoved方法是個空方法,什麼都沒實作,evicted如果是true則表示是為了釋放空間調用的,
//主要是在trimToSize方法中調用,如果是false則一般是被put,get,remove等方法調用。
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
           

OK,到目前為止LruCache的方法已經基本分析完畢,我們就來研究一下LruCache怎麼使用,一般情況下我們主要用來存儲bitmap的比較多,我們知道bitmap緩存的第三方架構比較多,我們就随便挑一個,我們現在最常用的volley架構一般是用來網絡請求的,其實它裡面也封裝了圖檔的緩存類ImageLoader,我們看到他裡面有個接口,我們看一下

/**
     * Simple cache adapter interface. If provided to the ImageLoader, it
     * will be used as an L1 cache before dispatch to Volley. Implementations
     * must not block. Implementation with an LruCache is recommended.
     */
    public interface ImageCache {
        public Bitmap getBitmap(String url);
        public void putBitmap(String url, Bitmap bitmap);
    }
           

他有一個put和get方法,我們看上面注釋的最後一行,意思是推薦使用LruCache實作。我們可以這樣來實作

package com.wld;

import android.graphics.Bitmap;
import android.util.LruCache;

import com.android.volley.toolbox.ImageLoader.ImageCache;

public class BitmapLRUCache implements ImageCache {

	private LruCache<String, Bitmap> mBitmapCache;

	public BitmapLRUCache() {
		this((int) Runtime.getRuntime().maxMemory() / 4);
	}

	public BitmapLRUCache(int maxSize) {
		mBitmapCache = new LruCache<String, Bitmap>(maxSize) {
			@Override
			protected int sizeOf(String key, Bitmap bitmap) {
				return bitmap.getRowBytes() * bitmap.getHeight();
			}
		};
	}

	private void entryRemoved() {
		//這裡可以實作二級緩存
	}

	@Override
	public Bitmap getBitmap(String url) {
		return mBitmapCache.get(url);
	}

	@Override
	public void putBitmap(String url, Bitmap bitmap) {
		mBitmapCache.put(url, bitmap);
	}
}
           

上面的entryRemoved方法其實是可以實作二級緩存的,我們可以在手機的SD中開辟一塊空間,用來儲存上面entryRemoved方法中移除的圖檔。邏輯是這樣的,當我們需要圖檔的時候首先從LruCache中取,如果沒有就從SD中取,如果SD卡中也沒有就從網絡上下載下傳,下載下傳完之後就儲存到LruCache中,如果LruCache中圖檔大小達到上限或者調用remove方法就會執行LruCache的entryRemoved方法,在entryRemoved方法中我們可以把移除的圖檔存儲到SD卡中,這樣下一次取的時候還是按照這個順序來取。網上還有一些實作方法和上面的差別是他沒有重寫entryRemoved方法,當從網上下載下傳完的時候,在LruCache和SD中都儲存了一份,這樣做也是可以的。我目前沒有發現google的實作SD卡緩存的類,不過第三方的倒是有很多,比如thinkandroid的DiskLruCache類,SmartAndroid的LruDiscCache類還有KJFrameForAndroid的DiskCache類都實作了磁盤緩存。大家有興趣的話可以自己去看一下,這裡就不在介紹。