LRU Cache
1. 概念解析,LRU Cache算法
- Lru Cache算法就是Least Recently Used,按照字面意思就是最近最少使用,用這種算法來實現緩存就比較合適了,當緩存滿了的時候,不經常使用的就直接刪除,挪出空間來緩存新的對象;
- 實現緩存的最關鍵的操作就是,添加和讀取以及刪除等操作了
- LRU 實現使用LinkedHashMap永久的緩存數據,那為什么要用這個呢?
- LinkedHashMap是雙向列表實現的,剛好在內部具有排序的功能,內部的accessorder代表了兩種模式,插入模式和訪問模式,false為訪問模式按照順序來實現(默認就是false),所以按照此種思路,則鏈表的最后段就是最少使用的緩存,比較方便來實現;
- LinkedHashMap是雙向循環列表來實現,默認容量大小16、負載因子0.75以及按照插入順序排序,不用我們管理擴容等問題;
- 添加和讀取數據:保證訪問順序排序,會將數據插入或者移動到鏈表的尾部,而且鏈表的刪除和增加速度比較快;
- LinkedHashMap遍歷順序是從頭到尾,這樣可以保證刪除最老的數據;
2. LRU cache的簡單使用
int maxMemory = (int) (Runtime.getRuntime().totalMemory()/1024);
int cacheSize = maxMemory/8;
mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight()/1024;
}
};
- 獲取到當前虛擬機的最大內存值,然后取1/8來當緩存;
- 注意單位的一致性sizeof()和cacheSize的單位要一直,上面為kb;
- sizeOf()是為了計算緩存對象大小的計算;
- 使用的時候你就可以當做一個map去使用就好了,只不過自動添加了擴容,緩存,以及幫你防止OOM的情況;
3. LRU Cache源碼解析
分析源碼主要從幾個方面來分析,創建,存取,刪除這三個方面來:
-
創建:
public class LruCache<K, V> { private final LinkedHashMap<K, V> map; /** Size of this cache in units. Not necessarily the number of elements. */ private int size; private int maxSize; private int putCount; private int createCount; private int evictionCount; private int hitCount; private int missCount; 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;
-
put方法是把內容放入到緩存中去
//給對應key緩存value,該value將被移動到隊頭。 public final V put(K key, V value) { //不可為空,否則拋出異常 if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { //插入的緩存對象值加1,記錄 put 的次數 putCount++; //增加已有緩存的大小,拿到鍵值對,計算出在容量中的相對長度,然后加上 size += safeSizeOf(key, value); //向map中加入緩存對象,如果 之前存在key 則返回 之前key 的value,記錄在 previous previous = map.put(key, value); //如果已有緩存對象,則緩存大小恢復到之前 if (previous != null) { // // 計算出 沖突鍵值 在容量中的相對長度,然后減去 size -= safeSizeOf(key, previous); } } //entryRemoved()是個空方法,可以自行實現,如果上面發生沖突 if (previous != null) { //previous值被剔除了,此次添加的 value 已經作為key的 新值,告訴 自定義 的 entryRemoved 方法 entryRemoved(false, key, previous, value); } //調整緩存大小 trimToSize(maxSize); return previous; } /* * 這是一個死循環, * 1.只有 擴容 的情況下能立即跳出 * 2.非擴容的情況下,map的數據會一個一個刪除,直到map里沒有值了,就會跳出 */ public void trimToSize(int maxSize) { while (true) { K key; V value; synchronized (this) { // 在重新調整容量大小前,本身容量就為空的話,會出異常的。 //如果map為空并且緩存size不等于0或者緩存size小于0,拋出異常 if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } // 如果是 擴容 或者 map為空了,就會中斷,因為擴容不會涉及到丟棄數據的情況 //如果緩存大小size小于最大緩存,或者map為空,不需要再刪除緩存對象,跳出循環 if (size <= maxSize || map.isEmpty()) { break; } //迭代器獲取第一個對象,即隊尾的元素,近期最少訪問的元素 Map.Entry<K, V> toEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); //刪除該對象,并更新緩存大小 map.remove(key); // 拿到鍵值對,計算出在容量中的相對長度,然后減去。 size -= safeSizeOf(key, value); evictionCount++; } //將最后一次刪除的最少訪問數據回調出去 entryRemoved(true, key, value, null); } }
put方法比較簡單只是把對象存儲,然后關鍵的方法是trimToSize(),調整緩存的,如果滿了就刪除然后更新
get獲取緩存
public final V get(K key) {
//key為空拋出異常
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//獲取對應的緩存對象
//get()方法會實現將訪問的元素更新到隊列頭部的功能,LinkHashMap 如果設置按照訪問順序的話,這里每次get都會重整數據順序
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
//判斷是否是訪問排序
if (lm.accessOrder) {
lm.modCount++;
//刪除此元素
remove();
//將此元素移動到隊列的頭部
addBefore(lm.header);
}
}
- 總結:
- LRUcache的源碼相對簡單,只要理解LinkedHashMap的原理,這個是非常簡單的實現;關鍵代碼是trimSize方法,每次添加完成之后調整緩存大小,get方法的也是調用的LinkedHashMap的get然后通過recordAcess來調整順序;