LruCache——解決OOM的利器

簡介

LruCache是Android 3.1的時候出現的,一般我們爲了兼容低版本會使用v4包下的。LruCache是一種緩存策略,持有的是強引用,但是會控制在一個峯值下。它內部維護了一個隊列,每當從中取出一個值時,該值就移動到隊列的頭部。當緩存已滿而繼續添加時,會將隊列尾部的值移除,方便GC。LruCache用於內存緩存,在避免程序發生OOM和提高執行效率有着良好表現。

LRU算法

和名字一樣,LruCache的實現正是基於LRU(Least Recently Used)算法。最近最少使用,我理解的就是最久遠的最少使用先被淘汰。下圖展示了LRU算法的核心思想,是最常用也是比較簡單的一種:
LruCache——解決OOM的利器_1.png

假設一個隊列的最大容量是5,那麼新進的元素會被添加到頭部,當隊列已滿時繼續添加會移除尾部的元素。值得注意的是,如果有一個不在隊頭的元素C又一次插入到隊列,因爲隊列中已經存在C,則不會重複插入,而是將C元素移動到頭部,相當於它的存在優先級當前是最高的。

LinkedHashMap

查看LruCache的源碼很容易就發現,只有一個容器類,就是LinkedHashMap。正好LinkedHashMap是一個雙向鏈表的數據結構,分爲訪問順序和插入順序。而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,並且是按訪問順序排序。也就是說,遍歷該隊列的entrySet,最先輸出的總是最早被插入並且最近沒有被訪問(操作)過的。最後被訪問的元素一定是作爲隊尾輸出的。要注意這裏的隊頭隊尾,和LruCache描述的隊列以及上圖中的隊列的區別,比較容易讓人矇蔽。

LruCache的初始化

以下是比較重要的幾個成員變量及代表的含義:

private int size;           //當前緩存的大小
private int maxSize;        //最大可緩存的大小

private int hitCount;       //命中緩存的次數
private int missCount;      //丟失緩存的次數

LruCache的構造函數中傳入參數爲可緩存的最大容量並賦值給maxSize,如果在初始化該LruCache對象時沒有重寫其sizeOf方法,那麼maxSize就代表了其內部的LinkedHashMap可以存儲的最大鍵值對數量。因爲sizeOf默認返回1,代表生產了一個鍵值對。注意只有maxSize和sizeOf返回值是同一個單位制緩存的判斷纔有意義。

put

LruCache插入元素(緩存值)的方法:

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) {
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }

    trimToSize(maxSize);
    return previous;
}
  1. 異常判斷說明LruCache不允許鍵或值爲null的操作。
  2. 在插入元素前會調用一次sizeOf,前面已經說過默認返回1,但一般我們會根據實際需要重寫。比如用LruCache存儲的value爲File,那麼sizeOf返回的就應該是當前對應該key的文件大小。
  3. 相應的size也要完成自增長,因爲當前緩存增加了,並且將對應的key-value插入到鏈表中。
  4. 二次檢查,如果該key已經存在鏈表中,此時新的value覆蓋後,size要減去之前的value所佔用的大小。
  5. 上面的操作都是同步的,爲了在多線程場景下保證size的準確性,否則緩存策略失效
  6. 如果是覆蓋了舊的value,LruCache對外提供了一個空方法entryRemoved
  7. 調用trimToSize,保證緩存不溢出。

trimToSize

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 || map.isEmpty()) {
                break;
            }

            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            size -= safeSizeOf(key, value);
            evictionCount++;
        }

        entryRemoved(true, key, value, null);
    }
}

該方法每插入一次元素就會被調用一次。整個方法就是一個無限循環,判斷當前緩存大小不大於最大容量則結束循環。否則就取出LinkedHashMap的entrySet的頭部,也就是最早被插入且最近未被訪問過的鍵值對並刪除,更新size。重複此步驟直到緩存<=最大容量。不得不說利用訪問順序的LinkedHashMap的特性完成LRU緩存,非常巧妙。

get

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);
    if (createdValue == null) {
        return null;
    }

    synchronized (this) {
        createCount++;
        mapValue = map.put(key, createdValue);

        if (mapValue != null) {
            // There was a conflict so undo that last put
            map.put(key, mapValue);
        } else {
            size += safeSizeOf(key, createdValue);
        }
    }

    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        trimToSize(maxSize);
        return createdValue;
    }
}
  1. key不可以爲null
  2. 如果map中存在與key相對應的value,則返回該value,並且緩存命中數+1。不存在,則緩存丟失數+1
  3. 不存在的話會嘗試根據該key創建一個value。創建方法默認返回null,需要自己實現。

其它

除此之外,LruCache還提供了手動清除指定緩存remove(K key),清除所有緩存evictAll()等方法供使用者調用。弄明白LinkedHashMap就很容易弄懂LruCache的實現了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章