主目錄見:Android高級進階知識(這是總目錄索引)
?今天我們來聊聊緩存策略相關的內(nèi)容,LruCache應該說是三級緩存策略會使用到的內(nèi)存緩存策略。今天我們就來扒一扒這里面的原理,同時也溫故溫故我們的數(shù)據(jù)結(jié)構(gòu)方面的知識。
一.目標
我們今天講的這個緩存策略,主要有幾個目的:
1.了解緩存的策略;
2.鞏固數(shù)據(jù)結(jié)構(gòu)相關的知識;
3.自己能實現(xiàn)一個緩存策略。
二.源碼解析
1.緩存策略
要來分析源碼,我們首先要先明白有哪幾種緩存淘汰算法,我們先來復習一下:
1.FIFO(First In First Out):先進先出;
2.LRU(Least Recently Used):最近最少使用;
3.LFU(Least Frequently Used):最不經(jīng)常使用。
這些都是什么呢?我們舉個例子,比如我們的緩存對象順序為:(隊尾)EDDCBABAEA(隊頭),那么如果這時候來了個A,這時候要淘汰一個對象,如果是FIFO
,這時候就會淘汰的E;如果是LRU
的話,這時候就會淘汰的D,因為D被使用過之后接下來再也沒有被使用過了;如果是LFU
的話,那么淘汰的就是C了,因為C就被使用過一次。這些就是我們?nèi)齻€緩存淘汰算法,我們知道我們的緩存是有限的,所以我們必須在新的對象進來的時候選擇一個優(yōu)秀的替換策略來替換緩存中的對象,這樣可以提高緩存的命中率,進而提高我們程序的效率。
2.LinkedHashMap
我們知道,我們的LRU算法可以用很多方法實現(xiàn),最常見的是用鏈表的形式,這里的LinkedHashMap就是雙向鏈表實現(xiàn)的,所以我們的LruCache是用的LinkedHashMap來實現(xiàn),我們首先看下LruCache的成員變量和構(gòu)造函數(shù):
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;//緩存內(nèi)容大小
private int maxSize;//最大的緩存大小
private int putCount;//put()方法被調(diào)用的次數(shù)
private int createCount;//create()方法被調(diào)用的次數(shù)
private int evictionCount;// 被置換出來的元素的個數(shù)
private int hitCount;//命中緩存中對象的次數(shù)
private int missCount;//未命中緩存中對象的次數(shù)
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
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);//第一個參數(shù)是初始化容量,第二個參數(shù)是加載因子默認是0.75,第三個為訪問順序
}
......
}
我們先來說說初始化容量和加載因子的關系,我們這里下來看下HashMap中的構(gòu)造函數(shù):
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
} else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
initialCapacity = DEFAULT_INITIAL_CAPACITY;
}
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Android-Note: We always use the default load factor of 0.75f.
// This might appear wrong but it's just awkward design. We always call
// inflateTable() when table == EMPTY_TABLE. That method will take "threshold"
// to mean "capacity" and then replace it with the real threshold (i.e, multiplied with
// the load factor).
threshold = initialCapacity;
init();
}
我們看到我們的初始容量為0的話,這里會使用默認的初始容量,然后如果我們進行擴容的時候會用到 float thresholdFloat = capacity * loadFactor即容量*加載因子來進行決定擴展后的容量,默認的加載因子0.75是實驗后的最佳數(shù)據(jù)。接著我們來看看LinkedHashMap是怎么實現(xiàn)的LRU算法的,我們先來LinkedHashMap的變量和構(gòu)造函數(shù):
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
private static final long serialVersionUID = 3801124242820219131L;
private transient LinkedHashMapEntry<K,V> header;
private final boolean accessOrder;
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
......
}
我們看到LinkedHashMap里面主要有LinkedHashMapEntry,這個是雙向鏈表的一個節(jié)點有前驅(qū)和后繼,我們可以來看看這個LinkedHashMapEntry節(jié)點:
private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
// These fields comprise the doubly linked list used for iteration.
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<K,V> next) {
super(hash, key, value, next);
}
.....
}
我們看到這里的LinkedHashMapEntry繼承的HashMapEntry,同時里面有before和after節(jié)點,這是為了擴展成雙向鏈表做的準備。我們來看下添加新的節(jié)點的方法最終會調(diào)用到createEntry方法:
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> old = table[bucketIndex];
LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
看懂這個方法之前,我們必須明確一下hashmap的數(shù)據(jù)結(jié)構(gòu),我們看下下面這個圖:
我們看到前面會有一個table數(shù)組用于存放各個entry鏈表的,然后LinkedHashMap又在此基礎上面增加了當前節(jié)點上面增加before和after的前驅(qū)和后繼節(jié)點的引用信息。為了大家更加清楚地知道這個雙鏈表結(jié)構(gòu),我們把雙鏈表抽取出來如下:
所以添加一個新的節(jié)點的時候會調(diào)用addbefore來添加,這個方法做的東西就是在頭部增加新的節(jié)點:
具體的代碼在addBefore()方法里面:
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
這里的existingEntry就是我們的header。所以我們可以看到新增的節(jié)點被插入到了首節(jié)點前面變成了首節(jié)點。我們剛才看到LruCache構(gòu)造函數(shù)里面LinkedHashMap的初始化的第三個參數(shù)accessOrder被賦值為true是什么意思呢?這個是為了記錄訪問的順序的,如果被訪問過了之后,這里true說明我們要把被訪問過的節(jié)點掉到首節(jié)點去。具體代碼可以看recordAccess()方法:
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
這個方法是在get方法中調(diào)用的,我們這里如果accessOrder為true的話,那么我們會先移除訪問節(jié)點,然后把它添加到首節(jié)點,說明我這個節(jié)點剛訪問過。到這里我們已經(jīng)明白了LinkedHashMap的工作原理了,那么我們接下來就來看看LruCache的源碼了。
3.LruCache源碼
熟悉了LinkedHashMap的數(shù)據(jù)結(jié)構(gòu),我們就很容易知道怎么用這個來實現(xiàn)LRU算法了,我們先來看看LruCache的get()方法的源碼:
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
//現(xiàn)在hashMap中查找有沒有這個key對應的節(jié)點(這個地方只要是get一次就會把命中的節(jié)點往首節(jié)點排)
mapValue = map.get(key);
if (mapValue != null) {
//如果命中的話那么命中+1,返回該值
hitCount++;
return mapValue;
}
//如果沒有命中的話那么沒命中+1
missCount++;
}
/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
*/
//嘗試去創(chuàng)建一個值,默認是空
V createdValue = create(key);
if (createdValue == null) {//如果不為沒有命名的key創(chuàng)建新值,則直接返回
return null;
}
// 接下來是如果用戶重寫了create方法后,可能會執(zhí)行到
synchronized (this) {
createCount++;//創(chuàng)建的數(shù)量增加
mapValue = map.put(key, createdValue););// 將剛剛創(chuàng)建的值放入map中,返回的值是在map中與key相對應的舊值(就是在放入new value前的old value)
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);//如果不為空,說明不需要我們所創(chuàng)建的值,所以又把返回的值放進去
} else {
size += safeSizeOf(key, createdValue);//為空,說明我們更新了這個key的值,需要重新計算大小
}
}
if (mapValue != null) {//上面放入的值有沖突
entryRemoved(false, key, createdValue, mapValue);// 通知之前創(chuàng)建的值已經(jīng)被移除,而改為mapValue
return mapValue;
} else {
trimToSize(maxSize);//沒有沖突時,因為放入了新創(chuàng)建的值,大小已經(jīng)有變化,所以需要修整大小
return createdValue;
}
}
我們看到LruCahe是可能被多個線程訪問的,所以讀取時候要適當加上鎖機制,當獲取不到key
對應的value
時候,他會調(diào)用create
方法,這個方法默認是返回null
的,除非我們重寫了create
方法,這個方法并沒有加鎖,所以在創(chuàng)建的過程中有可能其他線程已經(jīng)添加進去了這個值,所以在后面的時候會進行判斷是否已經(jīng)不為空了,如果不為空即刪除放入原來的值,沒有沖突就放入新值調(diào)整大小變化。我們來看下最后調(diào)整大小的代碼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) {
break;
}
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);
}
}
這個方法我們看到會判斷size<=maxSize不,如果小于則不用調(diào)整,如果大于了那么我們就會取出最老的Entry,進行刪除,然后置換的個數(shù)增加1。然后我們看下put的方法干了什么:
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
我們看到這個方法不難,主要的邏輯就是計算一下放入進去值的大小,然后加起來。同樣地,放進去map中,然后看是不是更新舊的值,如果是則把剛才加上的大小再減去,然后刪除舊的值跟maxSize調(diào)整一下總的大小。到這里我們大概已經(jīng)講完LruCache
的源碼了,我們也大概了解了整體的設計,其實我們自己也是可以寫出這樣一套代碼的,主要的還是數(shù)據(jù)結(jié)構(gòu)方面的知識。
總結(jié):其實整體的LruCache的實現(xiàn)并不會非常難,主要就是數(shù)據(jù)結(jié)構(gòu)的知識,我們可以根據(jù)這一套思想,我們也可以實現(xiàn)各種緩存策略,今天講的這個主要是內(nèi)存的緩存策略,到時我們可以來講講DiskLruCache是磁盤的緩存策略,希望能有所收獲哈。