android LruCache原始碼解析

許佳佳233發表於2016-11-30

#LruCache
LRU為最近最少使用演算法,LruCache顧名思義即為最近最少使用演算法下的快取機制。
LRU的目的是為了加快最近經常使用的資料從記憶體中取出的速度。

在android中LruCache在圖片快取中頻繁使用到,瞭解它絕對是必要的。
#Lru演算法實現
首先當然是看 LruCache這個類的原始碼,我們很容易發現LruCache類中僅僅是get,put等供開發者使用的方法,並未涉及連結串列等結構。(由於程式碼較長,筆者此處就不附上LruCache類的程式碼了)
但是其中卻又一個我們不常見的類,即LinkedHashMap,經過檢視,得出結論:

LruCache的底層是通過LinkedHashMap實現資料快取的。

LinkedHashMap是一個雙向連結串列,繼承了HashMap。

對HashMap不瞭解的可以看這篇文章:
http://blog.csdn.net/double2hao/article/details/53411594

根據LRU演算法的思路可知,LRU演算法定然是會在取出資料時對連結串列進行操作,從而加快下一次取出“經常使用的資料”的速度。
於是,我們定然先檢視get方法。

@Override public V get(Object key) {
        /*
         * This method is overridden to eliminate the need for a polymorphic
         * invocation in superclass at the expense of code duplication.
         */
        if (key == null) {
            HashMapEntry<K, V> e = entryForNullKey;
            if (e == null)
                return null;
            if (accessOrder)
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        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)
                    makeTail((LinkedEntry<K, V>) e);
                return e.value;
            }
        }
        return null;
    }

除去key等於null等意外的情況不看,我們可以發現,最關鍵的其實是這幾行程式碼:

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)
                    makeTail((LinkedEntry<K, V>) e);
                return e.value;
            }
        }

e即為我們要取出的那個結點,而makeTail在取出這個結點之前對其進行了操作,那麼makeTail定然便是實現LRU的方法。
接下來我們跳轉到makeTail這個方法中來。

 private void makeTail(LinkedEntry<K, V> e) {
        // Unlink e
        e.prv.nxt = e.nxt;
        e.nxt.prv = e.prv;

        // Relink e as tail
        LinkedEntry<K, V> header = this.header;
        LinkedEntry<K, V> oldTail = header.prv;
        e.nxt = header;
        e.prv = oldTail;
        oldTail.nxt = header.prv = e;
        modCount++;
    }

根據程式碼我們可知:把e這個結點從原連結串列中取出,放到連結串列的頭結點。讓它成為原連結串列的前一個結點,成為原連結串列的尾結點的最後一個結點。(原連結串列為雙向連結串列)
所以連結串列中經常使用的結點都會被排在前面,而不經常使用的結點都會被排在後面。
這時候我們再來看get中的關鍵程式碼:

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)
                    makeTail((LinkedEntry<K, V>) e);
                return e.value;
            }
        }

我們可以發現,get中查詢結點的方式是從前向後找的。
因此經常使用的結點查詢時間較短,而不經常使用的結點查詢時間較長。完全符合LRU演算法的最終目的。

#如何回收不經常使用的記憶體
這時候我們似乎對LruCache是如何運作的已經有了一個瞭解了,但是心中似乎還有一個梗:

記憶體是有限的,當記憶體滿了的時候,LruCache是如何回收不經常使用的記憶體的?

其實思路很清晰,我們分配給LruCache的記憶體大小是從哪裡傳進去的,從那裡定能尋找到蹤跡。於是便是來看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);
    }

我們發現傳進來的記憶體LruCache中用maxSize 這個屬性進行儲存。那麼我們只要找到maxSize 具體使用的地方便可以了。
於是我們可以發現在resize和put方法中都有使用到,並且都是把maxSize 傳入了trimToSize()這個方法中使用的。

public void resize(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

        synchronized (this) {
            this.maxSize = maxSize;
        }
        trimToSize(maxSize);
    }
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;
    }

於是我們可知,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) {
                    break;
                }

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

其中map是一個LinkedHashMap物件,為了方便後面的分析,我們放上它的eldest方法程式碼,其實就是取出最久未使用的那個結點。

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

trimToSize()的記憶體回收邏輯主要是以下幾行程式碼:

				Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);

即查詢到最久未使用的結點並remove。
那麼這個邏輯究竟在什麼情況下會執行呢?這段程式碼之前的兩個if給了我們很好的解釋:
1、記憶體中有資料
2、記憶體中的資料超過了maxSize即提供的最大記憶體量。

至於trimToSize()的while和synchronized 其實都比較好理解
while是為了讓記憶體一直低於maxSize即最大記憶體。
synchronized 保證了執行緒安全。

相關文章