經過使用 LruCache, 查看 LinkedHashMap 源碼, 分析 LRU 算法的具體實現細節.java
當序列達到設置的內存上限時, 丟棄序列中最近最少使用的元素.node
Android SDK 提供的使用了(Least Recently Used)最近最少使用算法的緩存類.android
編寫一個 LruCache, 用於緩存 Integer.算法
public class IntegerCache extends LruCache<String, Integer> {
public IntegerCache(int maxSize) {
super(maxSize);
}
@Override
protected int sizeOf(String key, Integer value) {
return Integer.SIZE;
}
}
複製代碼
// 最大容量爲 4 個 Integer
IntegerCache ca = new IntegerCache(4 * Integer.SIZE)
ca.put("1", 1);
ca.put("2", 2);
ca.put("3", 3);
ca.put("4", 4);
ca.get("4");
ca.put("5", 5);
ca.put("4", 4);
ca.put("6", 6);
複製代碼
緩存中內容:緩存
{1=1} // put 1
{1=1, 2=2} // put 2
{1=1, 2=2, 3=3} // put 3
{1=1, 2=2, 3=3, 4=4} // put 4
---
{1=1, 2=2, 3=3, 4=4} // get 4
{2=2, 3=3, 4=4, 5=5} // put 5
{2=2, 3=3, 5=5, 4=4} // put 4
{3=3, 5=5, 4=4, 6=6} // put 6
複製代碼
可見, 每次的 get
和 put
操做, 都會形成序列中的重排序, 最近使用的元素在末尾, 最近最少使用的元素在頭部, 當容量超過限制時會移出最近最少使用的元素.數據結構
public class LruCache<K, V> {
// 構造時就初始化的一個 LinkedHashMap
private final LinkedHashMap<K, V> map;
private int size; /* 記錄當前緩存佔用的內存大小 */
private int maxSize; /* 最多能緩存的內存大小 */
private int putCount; /* 記錄 put 調用的次數 */
private int createCount; /* 記錄 create 調用的次數 */
private int evictionCount; /* 記錄被丟棄的對象個數 */
private int hitCount; /* 記錄調用 get 時,緩存命中的次數 */
private int missCount; /* 記錄調用 get 時,緩存未命中的次數 */
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
// 初始容量爲0, 擴容係數爲 0.75, 排序模式: true 表示按訪問排序, false 表示按插入排序, SDK 實現裏固定爲 ture
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
複製代碼
public final V put(K key, V value) {
V previous;
synchronized (this) {
putCount++;
// 內存佔用記錄增長
size += safeSizeOf(key, value);
// 存入新的值, 並獲取 key 對應的舊值
previous = map.put(key, value);
if (previous != null) {
// 若是舊值存在, 就減去對應內存
size -= safeSizeOf(key, previous);
}
}
// 若是 size > maxSize, 就執行丟棄元素, 裁剪內存操做
trimToSize(maxSize);
return previous;
}
複製代碼
public final V get(K key) {
V mapValue;
synchronized (this) {
// 從緩存中獲取 key 對應的 value, 若是存在就直接返回
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
// 若是緩存中沒有, 就嘗試建立一個對應對象, 該方法由子類實現, 能夠返回 null
V createdValue = create(key);
if (createdValue == null) {
return null;
}
// 若是子類 create 返回了非 null 對象, 就把這個對象返回, 並插入到緩存中
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
// 上面 get 時獲得了 null 纔會走到這, 怎麼在插入時舊值又跑出來了 ?
if (mapValue != null) {
// 這裏應該是避免多線程訪問時, 在 get 獲取爲 null 以後, 其餘線程插入了對應的值, 因此這裏把其餘線程插入的值還原回去
map.put(key, mapValue);
} else {
// 若是沒有其餘插入, 就把新建立的內存佔用記帳
size += safeSizeOf(key, createdValue);
}
}
...
}
複製代碼
以上就是 LruCache
裏主要的方法了, 看完也沒發現與 LRU 算法有關的東西, 那 LRU 的具體實現確定就在 LinkedHashMap
裏了.多線程
// LinkedHashMap 的節點數據結構, 繼承自 HashMap.Node
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
複製代碼
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
// accessOrder 決定內部的排序順序
this.accessOrder = accessOrder;
}
複製代碼
public V get(Object key) {
Node<K,V> e;
// 調用父類 HashMap 的方法
if ((e = getNode(hash(key), key)) == null)
return null;
// 若是按訪問順序排序爲 ture, 則進行重排序
if (accessOrder)
// 將 e 移動到最後
afterNodeAccess(e);
return e.value;
}
複製代碼
能夠看到, 重點就是 afterNodeAccess
這個方法.ide
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, /* p 指向當前節點 e */
b = p.before, /* b 指向前一個節點 */
a = p.after; /* a 指向後一個節點 */
p.after = null; /* 當前節點 after 置 null */
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
複製代碼
初始狀態 | 移動指向 | 最終結果 |
---|---|---|
初始狀態 | 移動指向 | 最終結果 |
---|---|---|
初始狀態 | 移動指向 | 最終結果 |
---|---|---|
這種 case 不會作排序操做, 由於元素已經位於鏈表尾部了.this
在訪問元素以後, 經過 afterNodeAccess
排序以後, 被訪問的元素就移動到了鏈表的尾部.spa
LinkedHashMap 的 put 操做是直接調用父類 HashMap 的, HashMap 的 put 操做以後, 被插入的元素將會位於鏈表的尾部, 而後會調用 afterNodeInsertion
, 該方法在 LinkedHashMap 中的實現:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMapEntry<K,V> first;
// 若是 removeEldestEntry 爲 true, 則移出頭部的元素
// LinkedHashMap 中 removeEldestEntry 默認返回 false
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
複製代碼
因爲 LinkedHashMap
中 removeEldestEntry
默認返回 false, 因此 LinkedHashMap
的插入操做, 默認不會移出元素, 移出元素的操做實際在 LruCache
中的 trimToSize
實現.
在獲取和插入以後, LinkedHashMap 中的元素排列就會是: 最近最多使用的位於尾部, 最近最少使用的位於頭部.
trimToSize
trimToSize
目的在於當緩存大於設置的最大內存時, 會移出最近最少使用到的元素(在 LinkedHashMap
中就是頭部的元素):
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size <= maxSize) {
break;
}
// 該方法會返回 LinkedHashMap 的頭節點
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
// 移出這個節點
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
複製代碼
Android 提供的 LruCache
基於 LinkedHashMap
實現, 利用 LinkedHashMap
會在每次訪問元素以後, 將元素移動到序列末尾的特色, 保證了最近最多使用的元素位於尾部, 最近最少使用的元素位於頭部. 當緩存佔用達到設置的上限時, LruCache
就會移出 LinkedHashMap
中的頭節點.
LinkedHashMap
擴展 HashMap
, 實現了一套雙向鏈表機制, 保證了在元素的移動上和元素的查找上的時間複雜度都爲 .