理解 LruCache 機制

我愛宋慧喬發表於2019-02-25

本人只是 Android小菜一個,寫技術文件只是為了總結自己在最近學習到的知識,從來不敢為人師,如果裡面有些不正確的地方請大家盡情指出,謝謝!

1. 概述

由於 Android 為每個程式分配的可用記憶體空間都是有限的,如果程式使用的記憶體超過了所分配的限制就會出現記憶體溢位問題。同時,如果應用每使用一個資源都需要從本地或網路載入,這無疑會影響應用的效能,為了既能保證應用效能又能避免記憶體溢位,就出現記憶體快取技術

所謂記憶體快取技術指的是把一些資源快取在記憶體中,如果需要載入資源,首先到記憶體中去尋找,尋找到的話就直接使用,否則去本地或者網路去尋找。其中最重要的是記憶體快取技術要有一個合適的快取策略,即根據什麼策略把快取中的資源刪除,以保證快取空間始終在一個合理的範圍內。

LruCacheAndroid提供的一個標準的基於LRU,最近最少使用演算法的快取技術,它的使用方法已經在其他博文裡簡單介紹過了,這裡主要介紹它的實現機制。

2. LruChche 實現原理

LRU的全稱是Least Recently Used,最近最少使用LruCache的實現原理就是在其內部維護一個佇列,內部元素按照最近使用時間進行排序,隊首是最近最常使用的元素,隊尾是最近最少使用的元素,當快取中元素達到最大數量後,把最近最少使用的元素即隊尾元素從快取佇列中移除,從而保證快取始終在一個合理記憶體範圍內。

下圖簡單演示LruCache的過程:

LruCache 演示圖
從這個演示圖中可以發現:

  1. 每次新入隊的元素總是位於隊首;
  2. 隊尾元素是最久沒有使用過的元素;
  3. 當佇列中的元素被再次使用後,就會把該元素重新插入到隊首。

LruCache中使用LinkedHashMap來儲存元素,而 LinkedHashMap內部使用雙向連結串列來實現這樣的一個 LRU佇列,其具體實現在這裡就不詳細描述了,大家只要瞭解這點就可以了。

3. LruCache 關鍵實現

記憶體快取技術中最關鍵的實現主要包含三部分:

  • 如何把元素加入快取
  • 如何從快取中獲取元素
  • 如何在快取滿時刪除元素

3.1 LruCache 的初始化

在詳細講解LruCache的三個關鍵實現部分前,首先要知道LruCache 的初始化。 首先看下是如何在程式碼裡使用LruCache的:

    int maxMemory = (int) Runtime.getRuntime().maxMemory();
    LruCache<String, Bitmap> mCache = new LruCache<String, Bitmap>(maxMemory / 4) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
    };
複製程式碼

在這段示例程式碼裡,建立了一個LruCache示例並重寫了sizeOf方法。重寫sizeOf方法是因為它會被用來判斷快取的當前大小是否已經達到了預定義的快取大小,如果超過就需要從中移除最久沒有使用的元素。預設情況下sizeOf返回的時候元素個數,所以如果在建立LruCache時指定的快取中的元素個數而非記憶體空間就可以不重新sizeOf方法。

現在來看在建立LruCache的時候到底發生了什麼,其建構函式如下:

    /**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    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);
    }
複製程式碼

從建構函式裡發現,除了根據傳入的引數確定了快取的最大記憶體空間(也可能是元素數量)外,還定義了一個LinkedHashMap並把其中的第三個引數設定為trueLinkedHashMap的建構函式如下:

    /**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
複製程式碼

其中,引數分別是初始容量, 負載因子和排序方式,如果accessOrder被設定為true就表示是按照訪順序進行排序的,這也就保證了LruCache中的原生是按照訪問順序排序的。

所以在LruCache的初始化過程中,一方面確定了快取的最大空間,另一方面利用LinkedHashMap實現了LRU佇列。

3.2 LruCache 快取元素

要使用LruCache,首先需要把需要快取的資源加入到LruCache快取空間,在LruCache實現這一功能的是put介面,來看下是如何實現的:

    /**
     * Caches {@code value} for {@code key}. The value is moved to the head of
     * the queue.
     *
     * @return the previous value mapped by {@code key}.
     */
    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);
            }
        }
        // 如果是更新元素,需要發出通知,預設 entryRemoved 沒有實現。
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
        // 檢查快取大小是否達到限制,如果達到需要移除最久沒使用的元素。
        trimToSize(maxSize);
        return previous;
    }
複製程式碼

put方法整體邏輯比較簡單,就是把新元素放在隊首,更新當前快取大小,並使用trimToSize 來保證當前快取大小沒有超過限制,其程式碼如下:

    /**
     * @param maxSize the maximum size of the cache before returning. May be -1
     *     to evict even 0-sized elements.
     */
    private 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;
                }

                // BEGIN LAYOUTLIB CHANGE
                // get the last item in the linked list.
                // This is not efficient, the goal here is to minimize the changes
                // compared to the platform version.
                Map.Entry<K, V> toEvict = null;
                for (Map.Entry<K, V> entry : map.entrySet()) {
                    toEvict = entry;
                }
                // END LAYOUTLIB CHANGE

                if (toEvict == null) {
                    break;
                }

                // 找到對穩元素,即最久沒有使用的元素,並移除之。
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                // 移除元素後更新當前大小
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }
複製程式碼

trimToSize的邏輯也很簡單明瞭,在快取佇列中找到最近最久沒有使用的元素,把它從佇列中移除,直到快取大小滿足限制。由於最近最久沒有使用的元素一直位於隊尾,所以只要找到隊尾元素並把它移除即可。

3.3 LruCache 取元素

快取元素的最終目的是為了方便後續能從快取中更快地獲取需要元素,LruCache獲取元素是通過get方法來實現的,其程式碼如下:

    /**
     * Returns the value for {@code key} if it exists in the cache or can be
     * created by {@code #create}. If a value was returned, it is moved to the
     * head of the queue. This returns null if a value is not cached and cannot
     * be created.
     */
    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++;
        }

        /*
         * 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.
         */
        // 如果找不到元素就呼叫 create 去建立一個元素,預設 create 返回 null.
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);
            // 新建立的元素和佇列中已存在元素衝突,這個已存在元素是在 create的過程中新加入佇列的。
            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;
        }
    }
複製程式碼

get方法的邏輯也是很簡潔明瞭的,就是直接從快取佇列中獲取元素,如果查詢到就返回並更新元素位置到隊首,如果查不到就自己建立一個加入佇列,但考慮到多執行緒的情況,加入佇列是需要考慮衝突情況。

3.4 LruCache 移除元素

雖然LruCache可以在快取空間達到限制是自動把最近最久沒使用的元素從佇列中移除,但也可以主動去移除元素,使用的方法就是remove,其程式碼如下:

    /**
     * Removes the entry for {@code key} if it exists.
     *
     * @return the previous value mapped by {@code key}.
     */
    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;
    }
複製程式碼

remove的邏輯更加簡單,到快取佇列中找到元素,移除,並更新快取大小即可。

4. 總結

本文主要分析了LruCache的內部實現機制,由於LruCache本身的程式碼量比較小,分析起來難度也不大,但養成分析原始碼的習慣所代表的意義更大,讓我們一起 Reading The Fucking Source Code !

相關文章