Android提供的 LruCache 的分析

Android提供的 LruCache 的分析

前言

在平常的開發當中,咱們主要的工做就是把用戶想要看的信息經過界面展現出來,不免就要和數據打交道,對於一些用戶關心的數據,咱們確定是要每次都要從網絡拿最新的數據展現。css

可是對於一些圖片數據,若是咱們每次都從網絡讀取圖片未免就有點浪費資源了,不只會浪費用戶的流量,也會影響咱們 App 的性能,因此一般的作法就是對圖片作緩存處理。java

相信你們都聽過圖片的三級緩存,下面先講下什麼是三級緩存?算法

什麼是三級緩存

三級緩存主要由三部分構成:緩存

  • 內存
  • 硬盤
  • 網絡

咱們知道,在內存中對數據處理的速度是最快的,硬盤次之,而網絡上讀取數據,顯示的時候,也要加載到內存中而後進行展現。安全

因此,咱們徹底能夠將部分用戶關心的,最常用的圖片保存在內存和硬盤中,當下次展現的時候快速的進行展現,而且不用浪費用戶流量。網絡

好比咱們要加載一個圖片,地址是 url,若是實現了三級緩存,那麼咱們在要顯示圖片的時候,進行如下步驟:ide

上圖中就展現了一個完整的實現了三級緩存的圖片加載的流程,仔細分析下流程圖,相信你已經知道什麼是三級緩存是什麼了源碼分析

緩存的核心 LRU 算法

咱們知道,內存和硬盤空間是有限的,咱們在實現內存緩存和硬盤緩存的時候,不能夠無休止的往緩存中添加數據,必然是要設置和合適的空間去緩存數據,當咱們設置的空間滿的時候,咱們須要移除一部分數據,而後添加新的數據進入緩存。性能

這就遇到了一個問題:空間滿的時候,先移除哪些數據呢?this

確定是先移除用戶最不常用的數據,把用戶常用的數據留在緩存中,保證用戶能夠快速的訪問到數據,這就使用到了 LRU 算法

LRU(Least recently used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是「若是數據最近被訪問過,那麼未來被訪問的概率也更高,若是數據最近沒被,那麼將來被訪問的概率就比較低,優先刪除」。

接下來咱們看下 Lru 算法在 Android 中的應用 LruCache 是怎麼實現的(DiskLruCache 原理相似,本文不在將)

LruCache

內存緩存可使用 Android 3.1 之後提供的一個緩存類:LruCache,這個類實現了 LRU 算法。

官方描述

A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection.

If your cached values hold resources that need to be explicitly released, override entryRemoved(boolean, K, V, V).

If a cache miss should be computed on demand for the corresponding keys, override create(K). This simplifies the calling code, allowing it to assume a value will always be returned, even when there's a cache miss.

LruCache 是存儲了有限數量的強引用的緩存,每次訪問一個值的時候,會將其移動到隊列的頭部,當一個值添加到已經滿的隊列的時候,會將隊列尾部的元素移除掉,讓 GC 回收掉。

若是緩存的值明確的要知道已經釋放,須要重寫 entryRemoved(boolean, K, V, V) 方法,作一些本身的處理

若是緩存用沒有找到一個對應的值,能夠經過 create(K),簡化了調用代碼,容許即便是沒有找到對應值的狀況下可以返回一個值。

看下成員變量和構造方法

public class LruCache<K, V> {
        // LruCache 的核心 LinkedHashMap
    private final LinkedHashMap<K, V> map;
    // 當前緩存大小
    private int size;
    // 最大緩存大小
    private int maxSize;
    // 插入次數
    private int putCount;
    // 建立次數,只有重寫 create(K) 方法的時候會改變
    private int createCount;
    // 移除數據次數,緩存滿的時候,插入新數據的時候,移除舊數據的時候,會改變這個值.
    private int evictionCount;
    // 命中次數,也就是 get 查找到元素的次數
    private int hitCount;
    // 未命中次數,也就是 get 沒查找到元素的次數
    private int missCount;
    /** * @param maxSize 若是沒有重寫 sizeOf 方法,maxSize 就是緩存中元素的最大個數 * 若是重寫了 sizeOf 方法,則 maxSize 就是全部緩存元素大小(也就是每一個元素乘以自身大小的總和) */
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        // 建立了一個默認容量 默認負載因子 ,容許訪問排序的 LinkedHashMap.
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
}
複製代碼

上面的最主要的就是設置的 maxSize 以及內部的定義的容許訪問排序的 LinkedHashMap。

maxSize 在重寫了 sizeOf 方法的狀況下,表明的就是咱們每一個元素乘以自身大小以後累加的容許的最大值。

LinkedHashMap的對象 map 則是實現 LruCache 的核心,前面在 LinkedHashMap 源碼分析 中已經講了,若是在建立 LinkedHashMap 的時候,指定了 accessOrder 爲 true 的話,那麼就會在訪問 LinkedHashMap 的過程當中,會對內部的元素從新排序,這裏就是實現 LruCache 的關鍵部分。

雖然咱們設置了 accessOrder 爲 true 實現了訪問時的元素排序,可是還遠遠不夠,由於 LruCache 會在必定時候移除最久未訪問的元素,那達到什麼程度移除?怎麼移除?

答案是在 LruCache 中去實現的,下面就按傳統的方式來了解 LruCache 的增刪改查的相關操做,經過一個完整的流程分析,基本能瞭解整個 LruCache 的實現。

經常使用方法分析

使用緩存,確定要先把數據添加到緩存中,咱們才能訪問,在 LruCache 中添加緩存的操做是 put 方法:

put() 添加緩存

public final V put(K key, V value) {
    // 鍵值對不可爲空
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }
    // 舊值
    V previous;
    // 同步代碼塊,使用 this 也就是說同時只能由一個線程操做這個對象
    synchronized (this) {
        putCount++;
        // 先經過safeSizeOf方法計算當前傳入的 value 的大小,累加的 size
        size += safeSizeOf(key, value);
        // 把鍵值對插入到 LinkedHashMap 中,若是有返回值,說明存在相同的 key,取出舊值給 previous
        previous = map.put(key, value);
        // 若是存在舊值,則從當前大小中刪除舊值佔用的大小.
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }
    // 若是 存在舊值,至關於把舊值移除了,這裏調用 entryRemoved 方法.
    // entryRemoved 默認是空實現,若是用戶有需求,能夠本身實現,完成一些資源的釋放工做.
    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }
    // 這個是最關鍵的方法,用來計算當前大小是否符合要求.
    trimToSize(maxSize);
    // 返回舊值
    return previous;
}
複製代碼

在 put 方法裏面咱們看到了,使用 LruCache 緩存是不容許鍵值對爲空的,而且在執行插入操做的時候,使用了 Synchronized 關鍵字對代碼進行線程同步,保證了插入操做的線程安全。

而後計算了當前插入值的大小,累加到 size 上,執行完插入操做之後,若是以前存在相同的 key 值,則把以前元素的大小從 size 上面給移除掉。若是沒有存在,就什麼也不作。

若是用戶重寫了 entryRemoved 操做,也會回調 entryRemoved方法,讓用戶執行一些資源釋放等工做。

最後調用了trimToSize(maxSize) 方法,這個方法是個核心方法,主要計算當前大小是否超過了設置的最大值,超過了則會將最近最少使用的元素移除。

trimToSize() 控制緩存的容量

在 LruCache 裏面,控制緩存容量不超過咱們設置的最大值的關鍵點就是這個 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 || map.isEmpty()) {
                break;
            }
            // 使用 map.entrySet() 表明從 LinkedHashMap 的頭結點開始遍歷,在
            // 上篇文章裏面看了源碼,能夠參考下面的連接
            // 從頭開始遍歷,那隻取一次,toEvict 就是頭節點的元素
            Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
            // 要刪除元素的 key
            key = toEvict.getKey();
            // 要刪除元素的 value
            value = toEvict.getValue();
            // 使用 LinkedHashMap 的 remove 方法刪除指定元素
            map.remove(key);
             // 從新計算當前 size 的大小
            size -= safeSizeOf(key, value);
            // 移除次數+1
            evictionCount++;
        }
        // 調用用戶自定義的 entryRemoved() 若是用戶定義了的話
        entryRemoved(true, key, value, null);
    }
}
複製代碼

首先開啓了一個無限循環,在循環裏面的同步代碼塊裏面會判斷當前的容量 size 是否超過最大容量 maxSize。

若是沒超過,結束循環。 若是超過,就會遍歷內部的 LinkedHashMap 對象 map,這裏使用的是 map.entrySet(),在上一篇LinkedHashMap 源碼分析 裏面咱們對 LinkedHashMap 的遍歷作了簡單的介紹,map.entrySet() 最終是調用 LinkedHashIterator 裏面的 nextNode 拿到節點,而後在 LinkedEntryIterator 裏面從節點裏面 經過 nextNode() 拿到 entry 的值。

上篇源碼裏面講了,在 LinkedHashIterator 的構造方法裏面是從頭節點開始取值的,因此這裏的調用的 next 方法拿的就是頭節點。

因此在 trimToSize 方法裏面主要作的事情就是:若是容量沒超過最大值,返回,若是超過最大值,就依次移除頭節點元素,一直到容量知足設定的最大值。

remove() 刪除緩存

public final V remove(K key) {
    // 不容許 null 值
    if (key == null) {
        throw new NullPointerException("key == null");
    }
    // 刪除的元素
    V previous;
    // 同步代碼塊保證線程安全
    synchronized (this) {
        // 刪除元素,並把值賦給 previous
        previous = map.remove(key);
        //若是以前有 key 對應的值,將其減去
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }
    // 若是用戶重寫了entryRemoved 而且 以前有與 key 對應的值,執行entryRemoved。
    if (previous != null) {
        entryRemoved(false, key, previous, null);
    }
    return previous;
}
複製代碼

這裏也很簡單,住要是經過內部的 LinkedHashMap 移除元素,而後再把原來緩存中的對應的值刪掉。

get() 獲取緩存

public final V get(K key) {
    // 不容許 null key
    if (key == null) {
        throw new NullPointerException("key == null");
    }
    // value 的值
    V mapValue;
    // 同步代碼塊保證當前實例的線程安全
    synchronized (this) {
        // 經過 LinkedHashMap 的 get 方法去尋找
        mapValue = map.get(key);
        // 找到只,直接返回,命中值 +1 
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        // 沒找到,未命中次數+1
        missCount++;
    }
    // 這個地方意識,沒有經過 get 方法找到,可是你想要有返回值,那麼久能夠重寫 create 方法本身建立一個 返回值、。
    V createdValue = create(key);
    // 建立的值爲 null ,直接返回 null
    if (createdValue == null) {
        return null;
    }
    synchronized (this) {
        createCount++;
        //將createdValue加入到map中,而且將原來鍵爲key的對象保存到mapValue
        mapValue = map.put(key, createdValue);
        // 原來位置不爲空,
        if (mapValue != null) {
            // There was a conflict so undo that last put
            // 撤銷上一步的操做,依舊把原來的值放到緩存。,替換掉新建立的值
            map.put(key, mapValue);
        } else {
            // 原來key 對應的沒值,計算當前緩存大小。
            size += safeSizeOf(key, createdValue);
        }
    }
    // 至關於一個替換操做,先用 createdValue 替換原來的值,而後這裏移除掉 createdValue 。返回原來 key 對應的值。
    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        // 調用trimToSize方法看是否須要回收舊數據
        trimToSize(maxSize);
        return createdValue;
    }
}
複製代碼

get方法前半部分是,從 map 裏面取值,若是取到就返回。

若是沒取到,而且重寫了 create(K) 方法,就會先把 create(K) 方法建立的 value 保存到緩存,若是新建立的 value 保存的位置原來有值,就會替換回來。而且執行 entryRemoved 方法給調用者回調。

前面也講了,LruCache 會根據元素的訪問順序進行排序。其實這裏內部調用 LinkedHashMap 的 get 或者 put 方法的時候會調用到 afterNodeAccess 方法, 在 LinkedHashMap 的 afterNodeAccess 方法中對內部元素排序,這在上一篇 LinkedHashMap 中有講到。

evictAll清除所有緩存數據

public final void evictAll() {
    trimToSize(-1); // -1 will evict 0-sized elements
}
複製代碼

這裏仍是調用的 trimToSize 方法,傳入的 -1,前面分析過,trimToSize 方法內部有一個循環,會在執行了

if (size <= maxSize || map.isEmpty()) {
    break;
}
複製代碼

之後,纔會終止循環,這裏傳入 -1,也就是當 map.isEmpty() 的時候,終止循環,也就把緩存清空了。

最後

經過對前面文章的閱讀,相信你對 Android 提供給咱們的 LruCache 有了清除的認識。

歸納來講就是: LruCache 中維護了一個 LinkedHashMap,該 LinkedHashMap 建立的時候,設置了 accessOrder 爲 true,其內部元素不是已插入順序排序,而是以訪問順序排序的。當調用put()方法獲取數據的時候,會在內部的 map 中添加元素,並調用 trimToSize() 判斷緩存是否已滿,若是滿了就刪除 LinkedHashMap 中位於頭節點的元素,即近期最少訪問的元素。當調用 get() 方法訪問緩存對象時,就會調用 LinkedHashMap 的 get() 方法得到對應集合元素,進而調用 LinkedHashMap 內部實現的 afterNodeAccess 方法將元素移動到尾節點。

事實上,LRU 算法是一種算法,具體的實現仍是要看我的,只不過這裏 Google 爲咱們提供了實現好的 LruCache,咱們也是能夠本身實現一個相似的 LruCache 的。

最重要的仍是要懂思想啊。

相關文章
相關標籤/搜索