Android快取機制-LRU cache原理與用法

androad發表於2018-09-12

在使用Android圖片載入框架時,經常會提到三級快取,其中主要的是記憶體快取和檔案快取。 兩個快取都是用到了LruCache演算法,在Android分別對應:LruCacheDiskLruCache

LRU演算法

作業系統中進行記憶體管理中時採用一些頁面置換演算法,如LRU、LFU和FIFO等。
其中LRU(Least recently used,最近最少使用)演算法,核心思想是當快取達到上限時,會先淘汰最近最少使用的快取。這樣可以保證快取處於一種可控的狀態,有效的防止OOM的出現。

LruCache用法

LruCache是從Android3.1開始支援,目前已經在androidx.collection支援。

初始化

LruCache初始化:

int maxCache = (int) (Runtime.getRuntime().maxMemory() / 1024);
//初始化大小:記憶體的1/8
int cacheSize = maxCache / 8;
memorySize = new LruCache<String, Bitmap>(512) {
    @Override
    protected int sizeOf(String key, Bitmap value) {
        //重寫該方法,計算每張要快取的圖片大小
        return value.getByteCount() / 1024;
    }
};
複製程式碼

方法

LruCache結構圖

Android快取機制-LRU cache原理與用法

方法 描述
get(K) 通過K獲取快取
put(K,V) 設定K的值為V
remove(K) 刪除K快取
evictAll() 清除快取
resize(int) 設定最大快取大小
snapshot() 獲取快取內容的映象

LruCache實現原理

LruCache初始化只要宣告 new LruCache(maxSize),這個過程主要做了那些功能,看原始碼:

public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    //宣告最大快取大小
    this.maxSize = maxSize;
    //重點,LinkedHashMap,LruCache的實現依賴
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
複製程式碼

LruCache核心思想就是淘汰最近最少使用的快取,需要維護一個快取物件列表,排列方式按照訪問順序實現。
把最近訪問過的物件放在隊頭,未訪問的物件放在隊尾,當前列表達到上限的時候,優先會淘汰隊尾的物件。
如圖(盜用下圖片):

Android快取機制-LRU cache原理與用法
在Java中,LinkedHashMap比較適合實現這種演算法。

LinkedHashMap

LinkedHashMap是一個關聯陣列、雜湊表,它是執行緒不安全的,繼承自HashMap,實現Map<K,V>介面。
內部維護了一個雙向連結串列,在插入資料、訪問,修改資料時,會增加節點、或調整連結串列的節點順序,雙向連結串列結構可以實現插入順序或者訪問順序。 在LruCache初始化定義了this.map = new LinkedHashMap<K, V>(0, 0.75f, true)
LinkedHashMap的建構函式:

public LinkedHashMap(int initialCapacity,
                        float loadFactor,
                        boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
複製程式碼

布林變數accessOrder定義了輸出的順序,true為按照訪問順序,false為按照插入順序。 accessOrder設定為true正好滿足LRU演算法的核心思想。

LruCache原始碼分析

既然LruCache底層使用LinkedHashMap,下面我們來看看它怎麼實現快取的操作的。 原始碼分析是在Android API 28版本,

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 += safeSizeOf(key, value);
        //使用LinkedHashMap的put方法
        previous = map.put(key, value);
        if (previous != null) {
            //如果previous存在,減少對應快取大小
            size -= safeSizeOf(key, previous);
        }
    }

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

    //檢查快取大小,刪除最近最少使用的快取
    trimToSize(maxSize);
    return previous;
}
複製程式碼

put方法主要是新增快取物件後,呼叫trimToSize方法,保證快取大小,刪除最近最少使用的快取。具體的新增快取,通過LinkedHashMapput方法實現。 LinkedHashMap繼承自HashMap,沒有重寫put方法,呼叫的是HashMap的put方法,在HashMap的putVal方法中有呼叫建立新節點的newNode方法,LinkedHashMap重寫了該方法。

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMapEntry<K,V> p =
        new LinkedHashMapEntry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}
// link at the end of list
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
    LinkedHashMapEntry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}
複製程式碼

其中LinkedHashMapEntry定義:

static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
    LinkedHashMapEntry<K,V> before, after;
    LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;

/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
複製程式碼

LinkedHashMapEntry用來儲存資料,定義了before,after顯示上一個元素和下一個元素。
下面的操作:

LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(10, 0.75f, true);
map.put(1,1);
複製程式碼

Android快取機制-LRU cache原理與用法
再執行map.put(2,2)
Android快取機制-LRU cache原理與用法
繼續執行map.put(3,3)
Android快取機制-LRU cache原理與用法
如果put了相同key的話,會做什麼操作。這個一起放到get方法中講解。

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

    //基本不會執行到這裡,除了重寫create方法
    /*
        * 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;
    }
}
複製程式碼

這裡主要就是呼叫LinkedHashMap的get方法。 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;
}
複製程式碼

afterNodeAccess方法:

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMapEntry<K,V> last;
    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;
    }
}
複製程式碼

afterNodeAccess方法會將當前被訪問的節點e,移動到內部雙向連結串列的尾部。 在put方法,map已經有三個資料。 現在操作map.get(1),具體的邏輯在afterNodeAccess方法,看下每步操作後值的變化。

if (accessOrder && (last = tail) != e) {
        LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        ...
}        
複製程式碼
變數 before after
head 1 null 2
tail 3 2 null
last (tail)3 2 null
p 1 null null
b null
a 2 1 3
if (b == null)
    head = a;
else
    b.after = a;
if (a != null)
    a.before = b;
else
    last = b;
複製程式碼
變數 before after
head (a)2 null 3
tail 3 2 null
last 3 2 null
p 1 null null
b null
a 2 null 3
if (last == null)
    head = p;
else {
    p.before = last;
    last.after = p;
}
tail = p;
複製程式碼
變數 before after
head 2 null 3
tail 1 3 null
last 3 2 (p)1
p 1 (last)3 null
b null
a 2 null 3

最後的操作結果:

Android快取機制-LRU cache原理與用法

trimToSize

getput方法都會呼叫到

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);
    }
}
複製程式碼

mapsize大於maxSize,會一直迴圈刪除最近最少使用的快取物件,直到快取大小小於 maxSize

以上就是LruCache基本原理,理解了LinkedHashMap,可以更加輕鬆地理解LruCache原理。
DiskLruCache內部實現也有一部分基於LinkedHashMap

參考

相關文章