一、簡介
LruCache(Least Recently Used)算法的核心思想就是最近最少使用算法
。他在算法的內部維護了一個LinkHashMap的鏈表
,LinkedHashMap 是由數組+雙向鏈表
的數據結構來實現的,通過put數據的時候判斷是否內存已經滿了,如果滿了,則將最近最少使用的數據給剔除掉,從而達到內存不會爆滿的狀態。
通過上面這張圖,我們可以看到,LruCache算法內部其實是一個隊列的形式在存儲數據,先進來的數據放在隊列的尾部,後進來的數據放在隊列頭部,如果要使用隊列中的數據,那麼使得之後將其又放在隊列的頭部
,如果要存儲數據並且發現數據已經滿了,那麼便將隊列尾部的數據給剔除掉
,從而達到我們使用緩存的目的。這裏需要注意一點,隊尾存儲的數據就是我們最近最少使用的數據,也就是我們在內存滿的時候需要剔除掉的數據。
二、代碼分析
1、構造方法和參數
public class LruCache<K, V> {
//定義一個LinkedHashMap,有序的 map
private final LinkedHashMap<K, V> map;
private int size;//初始大小
private int maxSize;//最大容量
private int putCount;//插入個數
private int createCount;//創建個數
private int evictionCount;//回收個數
private int hitCount;//找到key的個數
private int missCount;//沒找到key的個數
//構造函數,傳遞進來一個最大容量值
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);
}
//設置cache的大小
public void resize(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
synchronized (this) {
this.maxSize = maxSize;
}
trimToSize(maxSize);
}
}
2、 get 方法分析
//如果通過key查找到value存在於cache中就直接返回或者通過create方法創建一個然後返回
//如果這個值被返回了,那麼它將移動到隊列的頭部
//如果一個值沒有被緩存同時也不能被創建則返回null
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key); // 獲取 Value
//找到對應值,命中+1,直接返回該值
if (mapValue != null) {
hitCount++;
return mapValue;
}
//否則未命中+1
missCount++;
}
//如果沒找到key對應的值那麼就嘗試創建一個,也許花費較長的時間
//並且創建後返回的map也許和之前的不同,如果創建的值和map中有衝突的話
//那麼我們就釋放掉創建的值,保留map中的值。
V createdValue = create(key);
//通過觀察後面的create()方法,可以看到直接return null;
//那麼我們需要想一想爲什麼源碼中是直接返回null呢?
//因爲LruCache常常作爲內存緩存而存在,所以當我們查找key找不到對應的value時
//這個時候我們應該從其他方面,比如文件緩存或者網絡中請求數據
//而不是我們隨便賦值創建一個值返回,所以這裏返回null是合理的。
//如果自己真的有需要的話,自己需要重寫create方法,手動創建一個值返回
if (createdValue == null) {
return null;
}
//走到這兒說明創建了一個不爲null的值
synchronized (this) {
createCount++;//創建個數+1
//把創建的value插入到map對應的key中
//並且將原來鍵爲key的對象保存到mapValue
mapValue = map.put(key, createdValue);
if (mapValue != null) {
//如果mapValue不爲空,說明原來key對應的是有值的,則撤銷上一步的put操作。
map.put(key, mapValue);
} else {
//加入新創建的對象之後需要重新計算size大小
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
//每次新加入對象都需要調用trimToSize方法看是否需要回收
trimToSize(maxSize);
return createdValue;
}
}
- LinkedHashMap 的 get 方法
public V get(Object key) {
Node < K, V > e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
get() 方法其實最關鍵就是 afterNodeAccess()
,現在重點分析:
// 這個方法的作用就是將剛訪問過的元素放到集合的最後一位
void afterNodeAccess(Node < K, V > e) {
LinkedHashMap.Entry < K, V > last;
if (accessOrder && (last = tail) != e) {
// 將 e 轉換成 LinkedHashMap.Entry
// b 就是這個節點之前的節點
// a 就是這個節點之後的節點
LinkedHashMap.Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;
// 將這個節點之後的節點置爲 null
p.after = null;
// b 爲 null,則代表這個節點是第一個節點,將它後面的節點置爲第一個節點
if (b == null) head = a;
// 如果不是,則將 a 上前移動一位
else b.after = a;
// 如果 a 不爲 null,則將 a 節點的元素變爲 b
if (a != null) a.before = b;
else last = b;
if (last == null) head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
3、put 方法分析
現在以註釋的方式來解釋該方法的原理。
//將key對應的value緩存起來,放在隊列的頭部
//返回key對應的之前的舊值
public final V put(K key, V value) {
// 如果 key 或者 value 爲 null,則拋出異常
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
// 加入元素的數量,在 putCount() 用到
putCount++;//插入數量+1
// 回調用 sizeOf(K key, V value) 方法,這個方法用戶自己實現,默認返回 1
size += safeSizeOf(key, value);
//得到key對應的前一個value,如果之前無值,返回null,如果有值,返回前一個值
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
// 該方法默認方法體爲空
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
//返回之前key對應的舊值value
return previous;
}
//根據maxSize來調整內存cache的大小,如果maxSize傳入-1,則清空緩存中的所有對象
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!");
}
//直到緩存大小 size 小於或等於最大緩存大小 maxSize,則停止循環
if (size <= maxSize) {
break;
}
// 取出 map 中第一個元素
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++;//回收個數+1
}
entryRemoved(true, key, value, null);
}
}
put() 方法其實重點就在於 trimToSize() 方法裏面,這個方法的作用就是判斷加入元素後是否超過最大緩存數,如果超過就清除掉最少使用的元素。
4、remove 方法
//從內存緩存中根據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;
}
三、總結
- 爲什麼LruCache內部原理的實現需要用到LinkHashMap來存儲數據吶?
因爲LinkHashMap內部是一個數組加雙向鏈表的形式來存儲數據,他能夠保證插入時候的數據和取出來的時候數據的順序的一致性。也就是說,我們以什麼樣的順序插入數據,就會以什麼樣的順序取出數據。並且更重要的一點是,當我們通過get方法獲取數據的時候,這個獲取的數據會從隊列中跑到隊列頭來,從而很好的滿足我們LruCache的算法設計思想。
- LruCache 其實使用了 LinkedHashMap 維護了強引用對象?
總緩存的大小一般是可用內存的 1/8,當超過總緩存大小會刪除最少使用的元素,也就是內部 LinkedHashMap 的頭部元素。
當使用 get() 訪問元素後,會將該元素移動到 LinkedHashMap 的尾部