Android內存緩存——理解LruCache和LinkedHashMap

博主最近在學習Bitmap高效加載和3級緩存(內存緩存、本地緩存和網絡緩存)管理。LruCache(least recent used cache)是一種高效且普遍使用的管理策略。因此,便開啓了LruCache源碼學習之旅。

注意,本文中涉及的LruCache源碼爲support v4包中的LruCache。

Table of Contents

1  LruCache結構

2  待開發者覆寫的方法

2.1  sizeOf

2.2  entryRemoved()回調

2.3  create()方法

3  公共方法——put()

4  公共方法——get()

5  公共方法——remove()

6  LinkedHashMap中LRU思想的實現

6.1  LinkedHashMap的數據結構

6.2  添加數據時,LRU思想的實現

6.3  Iterator.next()返回最老的數據

7  結語

1  LruCache結構

private final LinkedHashMap<K, V> map;

private int size;
private int maxSize;

private int putCount;
private int createCount;
private int evictionCount;
private int hitCount;
private int missCount;

map: 利用LinkedHashMap實現key-value存儲,LinkedHashMap是雙端鏈表,可按訪問順序或存儲順序來進行排序;鏈頭是“最老”的對象,鏈尾是“最年輕”的對象;

size:緩存的真實大小,該大小指的是帶有單位的大小,如byte或kb等;

maxSize:緩存真實大小的最大值;

putCount:用於記錄put()方法被調用的次數;

createCount:用於記錄create()方法被調用的次數;

evictionCount:用於記錄緩存中被驅逐項目的數量;

hitCount:調用get(key)方法時,若緩存中存在key對應的value,即命中,該變量記錄了緩存命中的次數;

missCount:調用get(key)方法時,若緩存中存在key對應的value,即未命中,該變量記錄了緩存未命中的次數;

上述變量均對應相關公開的get()方法。

2  待開發者覆寫的方法

2.1  sizeOf

方法源碼爲:

protected int sizeOf(K key, V value) {
        return 1;
 }

該方法的意義在於:開發者可按單位自己定義緩存中每個對象的大小,若緩存中存放的是Bitmap,則可寫爲:

protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount() / 1024;
}

那麼size對應的單位爲kb。

2.2  entryRemoved()回調

在LruCache中,該方法是個空方法,可由開發者自定義實現。

protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

當調用了remove()、get()、put()或內部調用了trimToSize()的時候,會在不同情況下刪除某個緩存,此時會回調entryRemoved;

根據各個參數,可以判斷回調的具體時機;

若evicted爲true,則回調發生在trimToSize()裏,表示要刪除一些緩存來控制緩存池的大小;這種情況下的刪除就叫eviction,表示驅逐、趕出。

若evicted爲false,則表示發生在remove、get或put內部。

若newValue不是null,那麼回調肯定發生在put()當中;否則發生在trimToSize或remove中。

2.3  create()方法

該方法的默認實現爲:

protected V create(K key) {
        return null;
}

若不覆寫,則表示緩存池針對key不會創建任何對象;

該方法只會get()中調用。

當用某個key來get(V key)一個值,但get不到對象時,會調用該方法,根據開發者的意圖緩存池是否需要自己來創建這個key對應的value。

調用該方法時,是線程不安全的;針對多線程,緩存池會選擇捨棄剛創建的對象;

什麼叫捨棄?看一下這種情況:在多線程的環境下,線程A在緩存池中get(key)得不到值,於是調用create(key)來創建這個值;若同時,線程B向緩存池put了該key對應的值;此時緩存池會捨棄掉線程A正在創建的對象,採納線程B已經put進入的value。

3  公共方法——put()

看一下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);
        previous = map.put(key, value);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }
    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }
    trimToSize(maxSize);
    return previous;
}

看一下put()方法的邏輯:

首先進入synchronized語塊,線程安全;

putCount++,記錄調用put()的次數;

size增加,記錄緩存的實際大小;

將緩存的value放進map裏,並獲得可能的舊值,若存在舊值,那麼記錄緩存實際大小的size將調整。

若存在舊值,將調用entryRemoved()方法,該方法由開發者實現,作爲替換了緩存值後的回調,可以看到,entryRemoved()方法已經不是線程安全的了。

調用trimToSize,比較size和maxSize,看是否需要通過刪除一些緩存來保持緩存池的大小。

返回替換的舊值。

其中trimToSize()方法源碼如下:

public void trimToSize(int maxSize) {
    while (true) {
        K key;
        V value;
        synchronized (this) {
            //......
            //......
            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);
    }
}

進入無限循環,且進入synchronized代碼塊進行線程保護;

當size小於等於maxSize或map爲空時,將直接退出循環;

通過LinkedHashMap的迭代器獲取下一個緩存,next在隊列中是緩存時間最長的、最少用到的項目,獲得後將它刪除;這裏也體現了least recent used的思想。

相應地減少緩存的真實大小,並讓evictionCount數量+1,記錄被驅逐的條目的數量;

離開synchronized代碼塊;

調用entryRemoved()方法,注意參數的設置代表了不同的意義,該方法由開發者實現,完成刪除一個條目後的回調;可以看見entryRemoved()調用都是非線程安全的。

繼續循環,直到緩存大小滿足要求。

4  公共方法——get()

get()方法的源碼如下:

public final V get(K key) {
    //......
    V mapValue;
    synchronized (this) {
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }
    synchronized (this) {
        createCount++;
        mapValue = map.put(key, createdValue);
        if (mapValue != null) {
            map.put(key, mapValue);
        } else {
            size += safeSizeOf(key, createdValue);
        }
    }
    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        trimToSize(maxSize);
        return createdValue;
    }
}

首先,進入synchronized語塊,線程安全;

從map中獲取值,若該值存在,hitCount +1,並返回取到的值。

若此時map中不存在該值,missCount +1。

離開synchronized語塊,線程不安全

若未命中,調用create()方法,由緩存池創建該key對應的value;若緩存池不創建該value,則直接返回null

若緩存池創建了該value,則再次進入synchronized語塊,線程安全;

此時createCount + 1;

map put()創建的新值;

若在create()過程中,其他線程針對該key,put()了某個value至緩存池,此時put()將返回一個非null值。那麼此時,將會把該值重新put()進map,保留這個值,並調用entryRemoved()捨棄剛纔create()創建的的新值。

若在create()過程中,其他線程沒有put()入value,則根據新創建的值,調整緩存池的大小。

注意,使用LinkedHashMap的get()方法時,返回的值將被重新插入到隊列的最前端。這裏也體現了least recent used的思想。

5  公共方法——remove()

源碼如下:

    public final V remove(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V previous;
        synchronized (this) {
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

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

        return previous;
    }

從map中刪除指定key的value;

若成功刪除,要調整緩存池的大小,以及調用entryRemoved回調。

6  LinkedHashMap中LRU思想的實現

在3中,LinkedHashMap.put()方法中,通過LinkedEntryIterator的next()中,直接返回了最老的對象,並將它刪除,實現了緩存的更新。

6.1  LinkedHashMap的數據結構

//The head (eldest) of the doubly linked list.
transient LinkedHashMapEntry<K,V> head;
// The tail (youngest) of the doubly linked list.
transient LinkedHashMapEntry<K,V> tail;

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

從註釋就可看出,head指向最老的對象,tail指向最新的對象;

LinkedHashMapEntry就是HashMap中的Node。

HashMap使用Node[] table通過數組+鏈表的形式存儲數據。Node中含有hash code、key和value。通過hash code可以找到Node鏈首在table[]中的位置。關於HashMap這裏就不做多的討論。

6.2  添加數據時,LRU思想的實現

LinkedHashMap.put()時直接使用HashMap的put() -> putVal()

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
   Node<K,V>[] tab; 
   Node<K,V> p; 
   int n, i;
   //table爲null時初始化table
   if ((tab = table) == null || (n = tab.length) == 0)
       n = (tab = resize()).length;
   //hash在table中沒有數據,直接加入node
   if ((p = tab[i = (n - 1) & hash]) == null)
       tab[i] = newNode(hash, key, value, null);
   //......
   //之後的流程不貼代碼,直接以註釋說明
   //hash碼在table中有數據時,需要比較hash碼和key
   //若相等,則替換已有值
   //若不等,則在該table[index]下的Node鏈中,隊尾加入新的Node
   afterNodeInsertion(evict);
   return null;
}

其中LinkedHashMap自己實現了newNode()和afterNodeInsertion。

newNode()源碼如下:

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

當LinkedHashMap裏還沒有數據時,head和tail都爲null,此時新加入的數據將賦給tail和head,二者此時指向同一個對象;

當LinkedHashMap裏有數據後,舊的tail讓after指向新數據,新數據成爲tail。

所以,這個過程就遵循 了LRU的思想,最老的數據在head,最新的數據在tail。

afterNodeInsertion()是在添加了數據之後調用,源碼如下:

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMapEntry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

removeEldestEntry()將通過size來判斷是否需要移除最老的數據;而最老的數據就在head。

綜上,完成了LinkedHashMap關於LRU的緩存更新。

6.3  Iterator.next()返回最老的數據

數組結構如下:

abstract class LinkedHashIterator {
    LinkedHashMapEntry<K,V> next;
    LinkedHashMapEntry<K,V> current;
    int expectedModCount;
    
    LinkedHashIterator() {
        next = head;
        expectedModCount = modCount;
        current = null;
    }
    //.......
}

nextNode()方法:

final LinkedHashMapEntry<K,V> nextNode() {
    LinkedHashMapEntry<K,V> e = next;
    //......
    current = e;
    next = e.after;
    return e;
}

可以看到,初始化時,直接把head賦值給next;

nextNode中直接返回的是當前的next。

7  結語

最後,做一下小結:

要使用LruCache,需開發者結合自己的需求分別實現sizeOf、create()和entryRemoved()。其中sizeOf最好實現一下,以區分size和count的意義,其他兩個方法可以選擇實現。

create()和entryRemoved()在LruCache中調用是非線程安全的,這一點需要開發者注意。

get()、put()和remove()的核心都是線程安全的;

LruCache實際是以LinkedHashMap爲依託,進行存取和放置。由於LinkedHashMap派生自HashMap,故它可通過數組+鏈表的方式存儲數據。

而LinkedHashMap中又維持了head和tail組成的雙端鏈表,使其實現了LRU的思想。

 

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