LruCache

一. LruCache基本原理

LRU全稱爲Least Recently Used,即最近最少使用。因爲緩存容量是有限的,當有新的數據須要加入緩存,但緩存的空閒空間不足的時候,如何移除原有的部分數據從而釋放空間用來存放新的數據。node

LRU算法就是當緩存空間滿了的時候,將最近最少使用的數據從緩存空間中刪除以增長可用的緩存空間來緩存新數據。這個算法的內部有一個緩存列表,每當一個緩存數據被訪問的時候,這個數據就會被提到列表尾部,每次都這樣的話,列表的頭部數據就是最近最不常使用的了,當緩存空間不足時,就會刪除列表頭部的緩存數據。面試

二. LruCache的使用

 1 //獲取系統分配給每一個應用程序的最大內存
 2 int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
 3 int cacheSize=maxMemory/8; 
 4 private LruCache<String, Bitmap> mMemoryCache;
 5 //給LruCache分配1/8 
 6 mMemoryCache = new LruCache<String, Bitmap>(mCacheSize){  
 7     //重寫該方法,來測量Bitmap的大小  
 8     @Override  
 9     protected int sizeOf(String key, Bitmap value) {  
10         return value.getRowBytes() * value.getHeight()/1024;  
11     }  
12 };

三. LruCache部分源碼解析

LruCache 利用 LinkedHashMap 的一個特性(accessOrder=true 基於訪問順序)再加上對 LinkedHashMap 的數據操做上鎖實現的緩存策略。算法

LruCache 的數據緩存是內存中的。緩存

  • 首先設置了內部 LinkedHashMap 構造參數 accessOrder=true, 實現了數據排序按照訪問順序。
  • LruCache類在調用get(K key) 方法時,都會調用LinkedHashMap.get(Object key) 。
  • 如上述設置了 accessOrder=true 後,調用LinkedHashMap.get(Object key) 都會經過LinkedHashMap的afterNodeAccess()方法將數據移到隊尾。
  • 因爲最新訪問的數據在尾部,在 put 和 trimToSize 的方法執行下,若是發生數據移除,會優先移除掉頭部數據

1.構造方法

 1  /**
 2      * @param maxSize for caches that do not override {@link #sizeOf}, this is
 3      *     the maximum number of entries in the cache. For all other caches,
 4      *     this is the maximum sum of the sizes of the entries in this cache.
 5      */
 6     public LruCache(int maxSize) {
 7         if (maxSize <= 0) {
 8             throw new IllegalArgumentException("maxSize <= 0");
 9         }
10         this.maxSize = maxSize;
11         this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
12     }

LinkedHashMap參數介紹:安全

  • initialCapacity 用於初始化該 LinkedHashMap 的大小。app

  • loadFactor(負載因子)這個LinkedHashMap的父類 HashMap 裏的構造參數,涉及到擴容問題,好比 HashMap 的最大容量是100,那麼這裏設置0.75f的話,到75的時候就會擴容。框架

  • accessOrder,這個參數是排序模式,true表示在訪問的時候進行排序( LruCache 核心工做原理就在此),false表示在插入的時才排序。ide

2.添加數據 LruCache.put(K key, V value)

 1 /**
 2      * Caches {@code value} for {@code key}. The value is moved to the head of
 3      * the queue.
 4      *
 5      * @return the previous value mapped by {@code key}.
 6      */
 7     public final V put(K key, V value) {
 8         if (key == null || value == null) {
 9             throw new NullPointerException("key == null || value == null");
10         }
11 
12         V previous;
13         synchronized (this) {
14             putCount++;
15              //safeSizeOf(key, value)。
16              //這個方法返回的是1,也就是將緩存的個數加1.
17             // 當緩存的是圖片的時候,這個size應該表示圖片佔用的內存的大小,因此應該重寫裏面調用的sizeOf(key, value)方法
18             size += safeSizeOf(key, value);
19             //向map中加入緩存對象,若緩存中已存在,返回已有的值,不然執行插入新的數據,並返回null
20             previous = map.put(key, value);
21             //若是已有緩存對象,則緩存大小恢復到以前
22             if (previous != null) {
23                 size -= safeSizeOf(key, previous);
24             }
25         }
26           //entryRemoved()是個空方法,能夠自行實現
27         if (previous != null) {
28             entryRemoved(false, key, previous, value);
29         }
30 
31         trimToSize(maxSize);
32         return previous;
33     }
  • 開始的時候確實是把值放入LinkedHashMap,無論超不超過你設定的緩存容量。
  • 根據 safeSizeOf方法計算 這次添加數據的容量是多少,而且加到size 裏 。
  • 方法執行到最後時,經過trimToSize()方法 來判斷size 是否大於maxSize。

能夠看到put()方法並無太多的邏輯,重要的就是在添加過緩存對象後,調用 trimToSize()方法,來判斷緩存是否已滿,若是滿了就要刪除近期最少使用的數據。函數

2.trimToSize(int maxSize)

 1 /**
 2      * Remove the eldest entries until the total of remaining entries is at or
 3      * below the requested size.
 4      *
 5      * @param maxSize the maximum size of the cache before returning. May be -1
 6      *            to evict even 0-sized elements.
 7      */
 8     public void trimToSize(int maxSize) {
 9         while (true) {
10             K key;
11             V value;
12             synchronized (this) {
13               //若是map爲空而且緩存size不等於0或者緩存size小於0,拋出異常
14                 if (size < 0 || (map.isEmpty() && size != 0)) {
15                     throw new IllegalStateException(getClass().getName()
16                             + ".sizeOf() is reporting inconsistent results!");
17                 }
18      //若是緩存大小size小於最大緩存,不須要再刪除緩存對象,跳出循環
19                 if (size <= maxSize) {
20                     break;
21                 }
22      //在緩存隊列中查找最近最少使用的元素,若不存在,直接退出循環,若存在則直接在map中刪除。
23                 Map.Entry<K, V> toEvict = map.eldest();
24                 if (toEvict == null) {
25                     break;
26                 }
27 
28                 key = toEvict.getKey();
29                 value = toEvict.getValue();
30                 map.remove(key);
31                 size -= safeSizeOf(key, value);
32                 //回收次數+1
33                 evictionCount++;
34             }
35 
36             entryRemoved(true, key, value, null);
37         }
38     }
39 
40 
41 /**
42  * Returns the eldest entry in the map, or {@code null} if the map is empty.
43  *
44  * Android-added.
45  *
46  * @hide
47  */
48 public Map.Entry<K, V> eldest() {
49     Entry<K, V> eldest = header.after;
50     return eldest != header ? eldest : null;
51 }

trimToSize()方法不斷地刪除LinkedHashMap中隊首的元素,即近期最少訪問的,直到緩存大小小於最大值。this

3.LruCache.get(K key)

 1  /**
 2      * Returns the value for {@code key} if it exists in the cache or can be
 3      * created by {@code #create}. If a value was returned, it is moved to the
 4      * head of the queue. This returns null if a value is not cached and cannot
 5      * be created.
 6      * 經過key獲取緩存的數據,若是經過這個方法獲得的須要的元素,那麼這個元素會被放在緩存隊列的尾部,
 7      * 
 8      */
 9     public final V get(K key) {
10         if (key == null) {
11             throw new NullPointerException("key == null");
12         }
13 
14         V mapValue;
15         synchronized (this) {
16               //從LinkedHashMap中獲取數據。
17             mapValue = map.get(key);
18             if (mapValue != null) {
19                 hitCount++;
20                 return mapValue;
21             }
22             missCount++;
23         }
24     /*
25      * 正常狀況走不到下面
26      * 由於默認的 create(K key) 邏輯爲null
27      * 走到這裏的話說明實現了自定義的create(K key) 邏輯,好比返回了一個不爲空的默認值
28      */
29         /*
30          * Attempt to create a value. This may take a long time, and the map
31          * may be different when create() returns. If a conflicting value was
32          * added to the map while create() was working, we leave that value in
33          * the map and release the created value.
34          * 譯:若是經過key從緩存集合中獲取不到緩存數據,就嘗試使用creat(key)方法創造一個新數據。
35          * create(key)默認返回的也是null,須要的時候能夠重寫這個方法。
36          */
37 
38         V createdValue = create(key);
39         if (createdValue == null) {
40             return null;
41         }
42         //若是重寫了create(key)方法,建立了新的數據,就講新數據放入緩存中。
43         synchronized (this) {
44             createCount++;
45             mapValue = map.put(key, createdValue);
46 
47             if (mapValue != null) {
48                 // There was a conflict so undo that last put
49                 map.put(key, mapValue);
50             } else {
51                 size += safeSizeOf(key, createdValue);
52             }
53         }
54 
55         if (mapValue != null) {
56             entryRemoved(false, key, createdValue, mapValue);
57             return mapValue;
58         } else {
59             trimToSize(maxSize);
60             return createdValue;
61         }
62     }

當調用LruCache的get()方法獲取集合中的緩存對象時,就表明訪問了一次該元素,將會更新隊列,保持整個隊列是按照訪問順序排序,這個更新過程就是在LinkedHashMap中的get()方法中完成的。

總結

  • LruCache中維護了一個集合LinkedHashMap,該LinkedHashMap是以訪問順序排序的。
  • 當調用put()方法時,就會在結合中添加元素,並調用trimToSize()判斷緩存是否已滿,若是滿了就用LinkedHashMap的迭代器刪除隊首元素,即近期最少訪問的元素。
  • 當調用get()方法訪問緩存對象時,就會調用LinkedHashMap的get()方法得到對應集合元素,同時會更新該元素到隊尾

面試題:LRU算法實現原理以及在項目中的應用

上面說到,最近最少使用將排除在列表以外,那這個列表是什麼?? 經過源碼得知,jdk中實現的LRU算法內部持有了一個LinkedHashMap:

 1 /**
 2  *一個基於LinkedHashMap的LRU緩存淘汰算法,
 3  * 這個緩存淘汰列表包含了一個最大的節點值,若是在列表滿了以後再有額外的值添加進來,
 4  * 則LRU(最近最少使用)的節點將被移除列表外
 5  * 
 6  * 當前類是線程安全的,全部的方法都被同步了
 7  */
 8 public class LRUCache<K, V> {
 9 
10     private static final float hashTableLoadFactor = 0.75f;
11     private LinkedHashMap<K, V> map;
12     private int cacheSize;
13     
14     //...
15 }

 

而LinkedHashMap內部節點的特性就是一個雙向鏈表,有頭結點和尾節點,有下一個指針節點和上一個指針節點;LRUCache中,主要是put() 與 get () 兩個方法,LinkedHashMap是繼承至HashMap,查看源碼得知LinkedHashMap並無重寫父類的put方法,而是實現了其中另一個方法,afterNodeInsertion() 接下來分析一下LinkedHashMap中的put() 方法:

 1  // 由LinkedHashMap 實現回調
 2     void afterNodeAccess(Node<K,V> p) { }
 3     void afterNodeInsertion(boolean evict) { }
 4     void afterNodeRemoval(Node<K,V> p) { }
 5 
 6 // HashMap.put()
 7  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 8                    boolean evict) {
 9         Node<K,V>[] tab; Node<K,V> p; int n, i;
10         if ((tab = table) == null || (n = tab.length) == 0)
11             n = (tab = resize()).length;
12         if ((p = tab[i = (n - 1) & hash]) == null)
13             tab[i] = newNode(hash, key, value, null);
14         else {
15             Node<K,V> e; K k;
16             if (p.hash == hash &&
17                 ((k = p.key) == key || (key != null && key.equals(k))))
18                 e = p;
19             else if (p instanceof TreeNode)
20                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
21             else {
22                 for (int binCount = 0; ; ++binCount) {
23                     if ((e = p.next) == null) {
24                         p.next = newNode(hash, key, value, null);
25                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
26                             treeifyBin(tab, hash);
27                         break;
28                     }
29                     if (e.hash == hash &&
30                         ((k = e.key) == key || (key != null && key.equals(k))))
31                         break;
32                     p = e;
33                 }
34             }
35             if (e != null) { // existing mapping for key
36                 V oldValue = e.value;
37                 if (!onlyIfAbsent || oldValue == null)
38                     e.value = value;
39                 afterNodeAccess(e);
40                 return oldValue;
41             }
42         }
43         ++modCount;
44         if (++size > threshold)
45             resize();
46         // 回調給LinkedHashMap ,evict爲boolean
47         afterNodeInsertion(evict);
48         return null;
49     }

能夠看到新增節點的方法,是由父類實現,並傳遞迴調函數afterNodeInsertion(evict) 給LinkedHashMap實現:

1 void afterNodeInsertion(boolean evict) { // 可能移除最老的節點
2         LinkedHashMap.Entry<K,V> first;
3         if (evict && (first = head) != null && removeEldestEntry(first)) {
4             K key = first.key;
5             removeNode(hash(key), key, null, false, true);
6         }
7     }

能夠看到 if 中有一個removeEldestEntry(first) 方法,該方法是給用戶去實現的,該怎樣去移除這個節點,最經常使用的是判斷當前列表的長度是否大於緩存節點的長度:

 1 this.map = new LinkedHashMap<K, V>(hashTableCapacity, LRUCache.hashTableLoadFactor, true) {
 2             // (an anonymous inner class)
 3 
 4             private static final long serialVersionUID = 1;
 5 
 6             @Override
 7             protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
 8                 // 返回爲true的話可能移除節點
 9                 return this.size() > LRUCache.this.cacheSize;
10             }
11         };

接下來就是get() 方法了:

 1 public V get(Object key) {
 2         Node<K,V> e;
 3         if ((e = getNode(hash(key), key)) == null)
 4             return null;
 5         if (accessOrder)
 6             // 調用HashMap給LinkedHashMap的回調方法
 7             afterNodeAccess(e);
 8         return e.value;
 9     }
10 // afterNodeAccess(e)
11  void afterNodeAccess(Node<K,V> e) { // move node to last 將節點移到最後
12         LinkedHashMap.Entry<K,V> last;
13         if (accessOrder && (last = tail) != e) {
14             LinkedHashMap.Entry<K,V> p =
15                 (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
16             p.after = null;
17             if (b == null)
18                 head = a;
19             else
20                 b.after = a;
21             if (a != null)
22                 a.before = b;
23             else
24                 last = b;
25             if (last == null)
26                 head = p;
27             else {
28                 p.before = last;
29                 last.after = p;
30             }
31             tail = p;
32             ++modCount;
33         }
34     }

因此在這個方法中,就將咱們上面說到的最近最常使用的節點移到最後,而最近最少使用的節點天然就排列到了前面,若是須要移除的話,就從前面刪除掉了節點。

總結

LRUCache使用,又是LinkedHashMap使用,LRU算法就是淘汰算法,其中內置了一個LinkedHashMap來存儲數據。圖片加載框架Glide,其中大部分算法都是LRU算法;有內存緩存算法和磁盤緩存算法(DiskLRUCache); 當緩存的內存達到必定限度時,就會從列表中移除圖片(固然Glide有活動內存和內存兩個,並非直接刪除掉)。

相關文章
相關標籤/搜索