談談LruCache源碼


LruCache,首先從名字就能夠看出它的功能。做爲較爲經常使用的緩存策略,它在平常開發中起到了重要的做用。例如Glide中,它與SoftReference 在Engine類中緩存圖片,能夠減小流量開銷,提高加載圖片的效率。在API12時引入android.util.LruCache,然而在API22時對它進行了修改,引入了android.support.v4.util.LruCache。咱們在這裏分析的是support包裏的LruCachejava


什麼是LruCache算法?

Lru(Least Recently Used),也就是最近最少使用算法。它在內部維護了一個LinkedHashMap,在put數據的時候會判斷指定的內存大小是否已滿。若已滿,則會使用最近最少使用算法進行清理。至於爲何要使用LinkedHashMap存儲,由於LinkedHashMap內部是一個數組加雙向鏈表的形式來存儲數據,也就是說當咱們經過get方法獲取數據的時候,數據會從隊列跑到隊頭來。反反覆覆,隊尾的數據天然是最少使用到的數據。

node

image


LruCache如何使用?


初始化

通常來講,咱們都是取運行時最大內存的八分之一來做爲內存空間,同時還要覆寫一個sizeOf的方法。特別須要強調的是,sizeOf的單位必須和內存空間的單位一致。android

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)緩存valuekey
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方法的做用在於判斷內存空間是否溢出。利用無限循環,將一個一個的最少使用的數據給剔除掉。ide

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的工做原理已經很清楚了吧!有什麼不對的地方但願你們可以指正。學無止境,你們一塊兒加油吧。

相關文章
相關標籤/搜索