理解 LruCache 機(jī)制

1. 概述

由于 Android 為每個(gè)進(jìn)程分配的可用內(nèi)存空間都是有限的,如果進(jìn)程使用的內(nèi)存超過了所分配的限制就會(huì)出現(xiàn)內(nèi)存溢出問題。同時(shí),如果應(yīng)用每使用一個(gè)資源都需要從本地或網(wǎng)絡(luò)加載,這無疑會(huì)影響應(yīng)用的性能,為了既能保證應(yīng)用性能又能避免內(nèi)存溢出,就出現(xiàn)內(nèi)存緩存技術(shù)。

所謂內(nèi)存緩存技術(shù)指的是把一些資源緩存在內(nèi)存中,如果需要加載資源,首先到內(nèi)存中去尋找,尋找到的話就直接使用,否則去本地或者網(wǎng)絡(luò)去尋找。其中最重要的是內(nèi)存緩存技術(shù)要有一個(gè)合適的緩存策略,即根據(jù)什么策略把緩存中的資源刪除,以保證緩存空間始終在一個(gè)合理的范圍內(nèi)。

LruCacheAndroid提供的一個(gè)標(biāo)準(zhǔn)的基于LRU,最近最少使用算法的緩存技術(shù),它的使用方法已經(jīng)在其他博文里簡單介紹過了,這里主要介紹它的實(shí)現(xiàn)機(jī)制。

2. LruChche 實(shí)現(xiàn)原理

LRU的全稱是Least Recently Used,最近最少使用,LruCache的實(shí)現(xiàn)原理就是在其內(nèi)部維護(hù)一個(gè)隊(duì)列,內(nèi)部元素按照最近使用時(shí)間進(jìn)行排序,隊(duì)首是最近最常使用的元素,隊(duì)尾是最近最少使用的元素,當(dāng)緩存中元素達(dá)到最大數(shù)量后,把最近最少使用的元素即隊(duì)尾元素從緩存隊(duì)列中移除,從而保證緩存始終在一個(gè)合理內(nèi)存范圍內(nèi)。

下圖簡單演示LruCache的過程:

LruCache 演示圖

從這個(gè)演示圖中可以發(fā)現(xiàn):

  1. 每次新入隊(duì)的元素總是位于隊(duì)首;
  2. 隊(duì)尾元素是最久沒有使用過的元素;
  3. 當(dāng)隊(duì)列中的元素被再次使用后,就會(huì)把該元素重新插入到隊(duì)首。

LruCache中使用LinkedHashMap來保存元素,而 LinkedHashMap內(nèi)部使用雙向鏈表來實(shí)現(xiàn)這樣的一個(gè) LRU隊(duì)列,其具體實(shí)現(xiàn)在這里就不詳細(xì)描述了,大家只要了解這點(diǎn)就可以了。

3. LruCache 關(guān)鍵實(shí)現(xiàn)

內(nèi)存緩存技術(shù)中最關(guān)鍵的實(shí)現(xiàn)主要包含三部分:

  • 如何把元素加入緩存
  • 如何從緩存中獲取元素
  • 如何在緩存滿時(shí)刪除元素

3.1 LruCache 的初始化

在詳細(xì)講解LruCache的三個(gè)關(guān)鍵實(shí)現(xiàn)部分前,首先要知道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();
        }
    };

在這段示例代碼里,創(chuàng)建了一個(gè)LruCache示例并重寫了sizeOf方法。重寫sizeOf方法是因?yàn)樗鼤?huì)被用來判斷緩存的當(dāng)前大小是否已經(jīng)達(dá)到了預(yù)定義的緩存大小,如果超過就需要從中移除最久沒有使用的元素。默認(rèn)情況下sizeOf返回的時(shí)候元素個(gè)數(shù),所以如果在創(chuàng)建LruCache時(shí)指定的緩存中的元素個(gè)數(shù)而非內(nèi)存空間就可以不重新sizeOf方法。

現(xiàn)在來看在創(chuàng)建LruCache的時(shí)候到底發(fā)生了什么,其構(gòu)造函數(shù)如下:

    /**
     * @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);
    }

從構(gòu)造函數(shù)里發(fā)現(xiàn),除了根據(jù)傳入的參數(shù)確定了緩存的最大內(nèi)存空間(也可能是元素?cái)?shù)量)外,還定義了一個(gè)LinkedHashMap并把其中的第三個(gè)參數(shù)設(shè)置為true,LinkedHashMap的構(gòu)造函數(shù)如下:

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

其中,參數(shù)分別是初始容量, 負(fù)載因子和排序方式,如果accessOrder被設(shè)置為true就表示是按照訪順序進(jìn)行排序的,這也就保證了LruCache中的原生是按照訪問順序排序的。

所以在LruCache的初始化過程中,一方面確定了緩存的最大空間,另一方面利用LinkedHashMap實(shí)現(xiàn)了LRU隊(duì)列。

3.2 LruCache 緩存元素

要使用LruCache,首先需要把需要緩存的資源加入到LruCache緩存空間,在LruCache實(shí)現(xiàn)這一功能的是put接口,來看下是如何實(shí)現(xiàn)的:

    /**
     * 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++;
            // 更新當(dāng)前緩存大小并把元素加入緩存隊(duì)列,新元素位于隊(duì)首。
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            // 如果是更新已存在元素,在增加新元素大小后,需要減去酒元素大小,以保持緩存大小正確。
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }
        // 如果是更新元素,需要發(fā)出通知,默認(rèn) entryRemoved 沒有實(shí)現(xiàn)。
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
        // 檢查緩存大小是否達(dá)到限制,如果達(dá)到需要移除最久沒使用的元素。
        trimToSize(maxSize);
        return previous;
    }

put方法整體邏輯比較簡單,就是把新元素放在隊(duì)首,更新當(dāng)前緩存大小,并使用trimToSize 來保證當(dāng)前緩存大小沒有超過限制,其代碼如下:

    /**
     * @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;
                }

                // 找到對(duì)穩(wěn)元素,即最久沒有使用的元素,并移除之。
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                // 移除元素后更新當(dāng)前大小
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

trimToSize的邏輯也很簡單明了,在緩存隊(duì)列中找到最近最久沒有使用的元素,把它從隊(duì)列中移除,直到緩存大小滿足限制。由于最近最久沒有使用的元素一直位于隊(duì)尾,所以只要找到隊(duì)尾元素并把它移除即可。

3.3 LruCache 取元素

緩存元素的最終目的是為了方便后續(xù)能從緩存中更快地獲取需要元素,LruCache獲取元素是通過get方法來實(shí)現(xiàn)的,其代碼如下:

    /**
     * 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) {
            // 從緩存中找到元素后返回,并更新到隊(duì)首。
            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.
         */
        // 如果找不到元素就調(diào)用 create 去創(chuàng)建一個(gè)元素,默認(rèn) create 返回 null.
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);
            // 新創(chuàng)建的元素和隊(duì)列中已存在元素沖突,這個(gè)已存在元素是在 create的過程中新加入隊(duì)列的。
            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);
            } else {
                // 加入新創(chuàng)建元素后需要更新緩存大小
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            // 檢查緩存空間
            trimToSize(maxSize);
            return createdValue;
        }
    }

get方法的邏輯也是很簡潔明了的,就是直接從緩存隊(duì)列中獲取元素,如果查找到就返回并更新元素位置到隊(duì)首,如果查不到就自己創(chuàng)建一個(gè)加入隊(duì)列,但考慮到多線程的情況,加入隊(duì)列是需要考慮沖突情況。

3.4 LruCache 移除元素

雖然LruCache可以在緩存空間達(dá)到限制是自動(dòng)把最近最久沒使用的元素從隊(duì)列中移除,但也可以主動(dòng)去移除元素,使用的方法就是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的邏輯更加簡單,到緩存隊(duì)列中找到元素,移除,并更新緩存大小即可。

4. 總結(jié)

本文主要分析了LruCache的內(nèi)部實(shí)現(xiàn)機(jī)制,由于LruCache本身的代碼量比較小,分析起來難度也不大,但養(yǎng)成分析源碼的習(xí)慣所代表的意義更大,讓我們一起 Reading The Fucking Source Code !

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容