Android LruCache 緩存機制實現原理

經過使用 LruCache, 查看 LinkedHashMap 源碼, 分析 LRU 算法的具體實現細節.java

LRU 算法描述

當序列達到設置的內存上限時, 丟棄序列中最近最少使用的元素.node

LruCache

Android SDK 提供的使用了(Least Recently Used)最近最少使用算法的緩存類.android

編寫一個 LruCache, 用於緩存 Integer.算法

public class IntegerCache extends LruCache<String, Integer> {
    public IntegerCache(int maxSize) {
        super(maxSize);
    }

    @Override
    protected int sizeOf(String key, Integer value) {
        return Integer.SIZE;
    }
}
複製代碼
// 最大容量爲 4 個 Integer
IntegerCache ca = new IntegerCache(4 * Integer.SIZE)
ca.put("1", 1);
ca.put("2", 2);
ca.put("3", 3);
ca.put("4", 4);
ca.get("4");
ca.put("5", 5);
ca.put("4", 4);
ca.put("6", 6);
複製代碼

緩存中內容:緩存

{1=1}                // put 1
{1=1, 2=2}           // put 2
{1=1, 2=2, 3=3}      // put 3
{1=1, 2=2, 3=3, 4=4} // put 4
---
{1=1, 2=2, 3=3, 4=4} // get 4
{2=2, 3=3, 4=4, 5=5} // put 5
{2=2, 3=3, 5=5, 4=4} // put 4
{3=3, 5=5, 4=4, 6=6} // put 6
複製代碼

可見, 每次的 getput 操做, 都會形成序列中的重排序, 最近使用的元素在末尾, 最近最少使用的元素在頭部, 當容量超過限制時會移出最近最少使用的元素.數據結構

LruCache 的構造

public class LruCache<K, V> {
    // 構造時就初始化的一個 LinkedHashMap
    private final LinkedHashMap<K, V> map;

    private int size;          /* 記錄當前緩存佔用的內存大小 */
    private int maxSize;       /* 最多能緩存的內存大小 */

    private int putCount;      /* 記錄 put 調用的次數 */
    private int createCount;   /* 記錄 create 調用的次數 */
    private int evictionCount; /* 記錄被丟棄的對象個數 */
    private int hitCount;      /* 記錄調用 get 時,緩存命中的次數 */
    private int missCount;     /* 記錄調用 get 時,緩存未命中的次數 */

    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        // 初始容量爲0, 擴容係數爲 0.75, 排序模式: true 表示按訪問排序, false 表示按插入排序, SDK 實現裏固定爲 ture
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
複製代碼

LruCache 插入元素

public final V put(K key, V value) {
    V previous;
    synchronized (this) {
        putCount++;
        // 內存佔用記錄增長
        size += safeSizeOf(key, value);
        // 存入新的值, 並獲取 key 對應的舊值
        previous = map.put(key, value);
        if (previous != null) {
            // 若是舊值存在, 就減去對應內存
            size -= safeSizeOf(key, previous);
        }
    }

    // 若是 size > maxSize, 就執行丟棄元素, 裁剪內存操做
    trimToSize(maxSize);
    return previous;
}
複製代碼

LurCache 獲取緩存

public final V get(K key) {
    V mapValue;
    synchronized (this) {
        // 從緩存中獲取 key 對應的 value, 若是存在就直接返回
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }

    // 若是緩存中沒有, 就嘗試建立一個對應對象, 該方法由子類實現, 能夠返回 null
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    // 若是子類 create 返回了非 null 對象, 就把這個對象返回, 並插入到緩存中
    synchronized (this) {
        createCount++;
        mapValue = map.put(key, createdValue);
        // 上面 get 時獲得了 null 纔會走到這, 怎麼在插入時舊值又跑出來了 ?
        if (mapValue != null) {
            // 這裏應該是避免多線程訪問時, 在 get 獲取爲 null 以後, 其餘線程插入了對應的值, 因此這裏把其餘線程插入的值還原回去
            map.put(key, mapValue);
        } else {
            // 若是沒有其餘插入, 就把新建立的內存佔用記帳
            size += safeSizeOf(key, createdValue);
        }
    }
    ...
}
複製代碼

以上就是 LruCache 裏主要的方法了, 看完也沒發現與 LRU 算法有關的東西, 那 LRU 的具體實現確定就在 LinkedHashMap 裏了.多線程

LinkedHashMap 的實現

  • 內部數據結構: 雙向鏈表
// LinkedHashMap 的節點數據結構, 繼承自 HashMap.Node
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);
    }
}
複製代碼

構造

public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    // accessOrder 決定內部的排序順序
    this.accessOrder = accessOrder;
}
複製代碼

獲取操做

public V get(Object key) {
    Node<K,V> e;
    // 調用父類 HashMap 的方法
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 若是按訪問順序排序爲 ture, 則進行重排序
    if (accessOrder)
        // 將 e 移動到最後
        afterNodeAccess(e);
    return e.value;
}
複製代碼

能夠看到, 重點就是 afterNodeAccess 這個方法.ide

訪問 node 以後的排序操做

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, /* p 指向當前節點 e */
        b = p.before,   /* b 指向前一個節點 */
        a = p.after;    /* a 指向後一個節點 */
        p.after = null; /* 當前節點 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;
    }
}
複製代碼

Case 1: 訪問元素的先後存在元素

初始狀態 移動指向 最終結果

Case 1.2: 訪問元素的先後存在多個元素

初始狀態 移動指向 最終結果

Case 2: 訪問元素的後面存在元素, 前面不存在

初始狀態 移動指向 最終結果

Case 3: 訪問元素的前面存在元素, 後面不存在

這種 case 不會作排序操做, 由於元素已經位於鏈表尾部了.this


在訪問元素以後, 經過 afterNodeAccess 排序以後, 被訪問的元素就移動到了鏈表的尾部.spa

插入操做

LinkedHashMap 的 put 操做是直接調用父類 HashMap 的, HashMap 的 put 操做以後, 被插入的元素將會位於鏈表的尾部, 而後會調用 afterNodeInsertion, 該方法在 LinkedHashMap 中的實現:

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMapEntry<K,V> first;
    // 若是 removeEldestEntry 爲 true, 則移出頭部的元素
    // LinkedHashMap 中 removeEldestEntry 默認返回 false
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}
複製代碼

因爲 LinkedHashMapremoveEldestEntry 默認返回 false, 因此 LinkedHashMap 的插入操做, 默認不會移出元素, 移出元素的操做實際在 LruCache 中的 trimToSize 實現.

在獲取和插入以後, LinkedHashMap 中的元素排列就會是: 最近最多使用的位於尾部, 最近最少使用的位於頭部.

LruCache 的 trimToSize

trimToSize 目的在於當緩存大於設置的最大內存時, 會移出最近最少使用到的元素(在 LinkedHashMap 中就是頭部的元素):

androidxref 上的源碼實現:

public void trimToSize(int maxSize) {
    while (true) {
        K key;
        V value;
        synchronized (this) {

            if (size <= maxSize) {
                break;
            }

            // 該方法會返回 LinkedHashMap 的頭節點
            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);
    }
}
複製代碼

總結

  • Android 提供的 LruCache 基於 LinkedHashMap 實現, 利用 LinkedHashMap 會在每次訪問元素以後, 將元素移動到序列末尾的特色, 保證了最近最多使用的元素位於尾部, 最近最少使用的元素位於頭部. 當緩存佔用達到設置的上限時, LruCache 就會移出 LinkedHashMap 中的頭節點.

  • LinkedHashMap 擴展 HashMap, 實現了一套雙向鏈表機制, 保證了在元素的移動上和元素的查找上的時間複雜度都爲 O(1).

相關文章
相關標籤/搜索