理解 LruCache 機制

本人只是 Android小菜一個,寫技術文檔只是爲了總結本身在最近學習到的知識,歷來不敢爲人師,若是裏面有些不正確的地方請你們盡情指出,謝謝!java

1. 概述

因爲 Android 爲每一個進程分配的可用內存空間都是有限的,若是進程使用的內存超過了所分配的限制就會出現內存溢出問題。同時,若是應用每使用一個資源都須要從本地或網絡加載,這無疑會影響應用的性能,爲了既能保證應用性能又能避免內存溢出,就出現內存緩存技術算法

所謂內存緩存技術指的是把一些資源緩存在內存中,若是須要加載資源,首先到內存中去尋找,尋找到的話就直接使用,不然去本地或者網絡去尋找。其中最重要的是內存緩存技術要有一個合適的緩存策略,即根據什麼策略把緩存中的資源刪除,以保證緩存空間始終在一個合理的範圍內。緩存

LruCacheAndroid提供的一個標準的基於LRU,最近最少使用算法的緩存技術,它的使用方法已經在其餘博文裏簡單介紹過了,這裏主要介紹它的實現機制。網絡

2. LruChche 實現原理

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

下圖簡單演示LruCache的過程: app

LruCache 演示圖
從這個演示圖中能夠發現:

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

LruCache中使用LinkedHashMap來保存元素,而 LinkedHashMap內部使用雙向鏈表來實現這樣的一個 LRU隊列,其具體實如今這裏就不詳細描述了,你們只要瞭解這點就能夠了。ide

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 !

相關文章
相關標籤/搜索