1. 程式人生 > >LruCache原始碼解析

LruCache原始碼解析

LruCache

之前分析過Lru演算法的實現方式:HashMap+雙向連結串列,參考連結:LRU演算法&&LeetCode解題報告

這裡主要介紹Android SDK中LruCache快取演算法的實現, 基於Android5.1版本原始碼.

GitHub

參考實現了LruCache快取類,歡迎大家Fork使用.GitHub-LruCache

建構函式

LruCache只有一個建構函式,並且有一個必傳引數:

public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw
new IllegalArgumentException("maxSize <= 0"); } // 初始化最大快取大小. this.maxSize = maxSize; // 初始化LinkedHashMap.其中: // 1. initialCapacity, 初始大小. // 2. loadFactor, 負載因子. // 3. accessOrder, true:基於訪問順序排序, false:基於插入順序排序. this.map = new LinkedHashMap<K, V>(0, 0.75f, true); }

get方法

LruCache的get方法原始碼如下:

public final V get(K key) {
    // lru的key不能為null
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        // LinkedHashMap每次get會基於訪問順序重新排序
        mapValue = map.get(key);
        if (mapValue != null
) { // 命中,命中數+1,且返回命中資料 hitCount++; return mapValue; } // 未命中數+1 missCount++; } // 一般LruCache類都不會重寫create方法的,所以下面的邏輯不需要care. 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; } }

從上述程式碼中,我們並沒有看到LruCache演算法的實現,即訪問一個元素時,如果元素命中,則需要將元素保護起來,避免儲存空間不夠時立刻被刪除.
其實,這塊操作是由LinkedHashMap實現的,原始碼如下:

@Override public V get(Object key) {
    // 不需要考慮key為null的情況,因為LruCache禁止key為null.
    if (key == null) {
        HashMapEntry<K, V> e = entryForNullKey;
        if (e == null)
            return null;
        if (accessOrder)
            makeTail((LinkedEntry<K, V>) e);
        return e.value;
    }

    // 根據key計算hash值,用於定位陣列中的槽
    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    // HashMap的資料結構為: 陣列 + 連結串列
    // 首先,根據key的hash值定義陣列儲存下標index.
    // 陣列的每個元素是HashMapEntry,以連結串列的形式組織在一起,是一種解決衝突的方案.
    for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
            e != null; e = e.next) {
        K eKey = e.key;
        if (eKey == key || (e.hash == hash && key.equals(eKey))) {
            if (accessOrder)
                // 命中同時,accessOrder為true,代表基於訪問順序排序,需要將當前訪問的節點移動到尾部,實現LRU的get演算法精髓.
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        }
    }
    return null;
}

其中,makeTail的原始碼就是將當前訪問的節點移動到連結串列尾部,非常簡單的連結串列操作,註釋原始碼如下:

private void makeTail(LinkedEntry<K, V> e) {
    // 將e從連結串列中斷開
    e.prv.nxt = e.nxt;
    e.nxt.prv = e.prv;

    // 獲取當前連結串列的頭部
    LinkedEntry<K, V> header = this.header;
    // 獲取當前連結串列的尾部
    LinkedEntry<K, V> oldTail = header.prv;

    // 將e插入到oldTail之後,作為連結串列最新的尾部.
    e.nxt = header;
    e.prv = oldTail;
    oldTail.nxt = header.prv = e;
    modCount++;
}

為什麼將訪問的節點移動到當前連結串列的尾部就能對該元素起到保護作用呢?

答: 因為當Cache空間不夠時,刪除元素是從連結串列頭部開始進行的.想要更具體的瞭解,就需要看一下LruCache的put函式原始碼了.

put方法

LruCache的put原始碼比較簡單,註釋原始碼如下:

public final V put(K key, V value) {
    // 禁止存入的key或者value為null,否則丟擲空指標異常.
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

    V previous;
    synchronized (this) {
        // 存放數+1
        putCount++;
        // 累加儲存容量
        size += safeSizeOf(key, value);
        // 呼叫HashMap的put方法進行儲存
        previous = map.put(key, value);
        if (previous != null) {
            // 如果是同一個key,value替換的情況,則需要減去該previous值的大小.
            size -= safeSizeOf(key, previous);
        }
    }

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

    // 計算是否超size,如果超size,需要進行刪除操作.
    trimToSize(maxSize);
    return previous;
}

通過原始碼我們發現put原始碼的實現只是呼叫了HashMap的put方法,HashMap的put方法實現機制:

  1. 如果key本身存在,則替換value.
  2. 如果key不存在,則使用頭插法將鍵值對插入.

HashMap的put方法原始碼如下:

@Override public V put(K key, V value) {
    if (key == null) {
        return putValueForNullKey(value);
    }

    // 計算key對應的hash值
    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    // 根據key的hash值獲取key在陣列中儲存的index下標
    int index = hash & (tab.length - 1);
    // 首先遍歷陣列index槽中的連結串列,判斷該key是否已經儲存
    for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
        if (e.hash == hash && key.equals(e.key)) {
            // key之前存在,直接替換value
            preModify(e);
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }

    // key不存在,直接插入操作
    modCount++;
    // 擴容
    if (size++ > threshold) {
        tab = doubleCapacity();
        index = hash & (tab.length - 1);
    }
    // 尾插法插入<key,value>鍵值對(ps:非常感謝@MillerJK同學的指導,這裡是LinkedHashMap覆寫了HashMap的addNewEntry實現)
    addNewEntry(key, value, hash, index);
    return null;
}

這裡講解一下LinkedHashMap中addNewEntry的實現:

@Override void addNewEntry(K key, V value, int hash, int index) {
    // 獲取頭結點
    LinkedEntry<K, V> header = this.header;

    // 用LinkedHashMap實現Lru演算法,頭部節點為最近最久沒有被訪問的節點,這裡判斷是否需要刪除節點,以有空間來存放新節點
    LinkedEntry<K, V> eldest = header.nxt;
    if (eldest != header && removeEldestEntry(eldest)) {
        remove(eldest.key);
    }

    // 獲取尾節點(ps:用LinkedHashMap實現Lru演算法,尾節點為最近被訪問的節點)
    LinkedEntry<K, V> oldTail = header.prv;
    // 根據插入的資料構造最新的尾節點
    LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
            key, value, hash, table[index], header, oldTail);
    // 採用尾插法將新的節點插入到尾部
    table[index] = oldTail.nxt = header.prv = newTail;
}

接下來,我們還需要關注一下trimToSize函式實現,這也是LRU快取替換策略實現的關鍵.

trimToSize方法

public void trimToSize(int maxSize) {
    // 一個死迴圈,迴圈到容量不超過建構函式中定義的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;
            }

            // 獲取對頭元素,也是最近最久沒訪問過的元素(eldest為包訪問許可權)
            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);
    }
}

由於eldest有hide註解,為包訪問許可權,但是通過前面的分析,我們就可以知道eldest是獲取對頭元素:

public Entry<K, V> eldest() {
    LinkedEntry<K, V> eldest = header.nxt;
    return eldest != header ? eldest : null;
}