LruCache,首先從名字就可以看出它的功能。作為較為常用的緩存策略,它在日常開發(fā)中起到了重要的作用。例如Glide中,它與SoftReference 在Engine類中緩存圖片,可以減少流量開銷,提升加載圖片的效率。在API12時引入android.util.LruCache,然而在API22時對它進行了修改,引入了android.support.v4.util.LruCache。我們在這里分析的是support包里的LruCache
什么是LruCache算法?
Lru(Least Recently Used),也就是最近最少使用算法。它在內(nèi)部維護了一個LinkedHashMap,在put數(shù)據(jù)的時候會判斷指定的內(nèi)存大小是否已滿。若已滿,則會使用最近最少使用算法進行清理。至于為什么要使用LinkedHashMap存儲,因為LinkedHashMap內(nèi)部是一個數(shù)組加雙向鏈表的形式來存儲數(shù)據(jù),也就是說當我們通過get方法獲取數(shù)據(jù)的時候,數(shù)據(jù)會從隊列跑到隊頭來。反反復(fù)復(fù),隊尾的數(shù)據(jù)自然是最少使用到的數(shù)據(jù)。
LruCache如何使用?
初始化
一般來說,我們都是取運行時最大內(nèi)存的八分之一來作為內(nèi)存空間,同時還要覆寫一個sizeOf的方法。特別需要強調(diào)的是,sizeOf的單位必須和內(nèi)存空間的單位一致。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(maxMemory / 8) {
@Override
protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
API
| 公共方法 | |
|---|---|
final int |
createCount()返回返回值的次數(shù)create(Object)。 |
final void |
evictAll()清除緩存,調(diào)用entryRemoved(boolean, K, V, V)每個刪除的條目。 |
final int |
evictionCount()返回已被驅(qū)逐的值的數(shù)量。 |
final V |
get(K key)返回key緩存中是否存在的值,還是可以創(chuàng)建的值#create。 |
final int |
hitCount()返回返回get(K)已存在于緩存中的值的次數(shù)。 |
final int |
maxSize()對于不覆蓋的高速緩存sizeOf(K, V),這將返回高速緩存中的最大條目數(shù)。 |
final int |
missCount()返回get(K)返回null或需要創(chuàng)建新值的次數(shù)。 |
final V |
put(K key, V value)緩存value的key。 |
final int |
putCount()返回put(K, V)調(diào)用的次數(shù)。 |
final V |
remove(K key)刪除條目(key如果存在)。 |
void |
resize(int maxSize)設(shè)置緩存的大小。 |
final int |
size()對于不覆蓋的高速緩存sizeOf(K, V),這將返回高速緩存中的條目數(shù)。 |
final Map<K, V> |
snapshot()返回緩存的當前內(nèi)容的副本,從最近最少訪問到最近訪問的順序排序。 |
final String |
toString() |
void |
trimToSize(int maxSize)刪除最舊的條目,直到剩余條目的總數(shù)等于或低于請求的大小。 |
LruCache源碼分析
我們接下里從構(gòu)造方法開始為大家進行講解:
構(gòu)造函數(shù)
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
} else {
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75F, true);
}
}
構(gòu)造函數(shù)一共做了兩件事。第一節(jié):判斷maxSize是否小于等于0。第二件,初始化maxSize和LinkedHashMap。沒什么可說的,我們接著往下走。
safeSizeOf(測量元素大?。?/h4>
private int safeSizeOf(K key, V value) {
int result = this.sizeOf(key, value);
if (result < 0) {//判空
throw new IllegalStateException("Negative size: " + key + "=" + value);
} else {
return result;
}
}
sizeOf (測量元素大小)
private int safeSizeOf(K key, V value) {
int result = this.sizeOf(key, value);
if (result < 0) {//判空
throw new IllegalStateException("Negative size: " + key + "=" + value);
} else {
return result;
}
}
這個方法一定要覆寫,否則存不進數(shù)據(jù)。
protected int sizeOf(@NonNull K key, @NonNull V value) {
return 1;
}
put方法 (增加元素)
@Nullable
public final V put(@NonNull K key, @NonNull V value) {
if (key != null && value != null) {
Object previous;
synchronized(this) {
++this.putCount;//count為LruCahe的緩存?zhèn)€數(shù),這里加一
this.size += this.safeSizeOf(key, value);//加上這個value的大小
previous = this.map.put(key, value);//存進LinkedHashMap中
if (previous != null) {//如果之前存過這個key,則減掉之前value的大小
this.size -= this.safeSizeOf(key, previous);
}
}
if (previous != null) {
this.entryRemoved(false, key, previous, value);
}
this.trimToSize(this.maxSize);//進行內(nèi)存判斷
return previous;
} else {
throw new NullPointerException("key == null || value == null");
}
}
在synchronized代碼塊里,進入的就是一次插入操作。我們往下,俺老孫定眼一看,似乎trimToSize這個方法有什么不尋常的地方?
trimToSize (判斷是否內(nèi)存溢出)
public void trimToSize(int maxSize) {
while(true) {//這是一個無限循環(huán),目的是為了移除value直到內(nèi)存空間不溢出
Object key;
Object value;
synchronized(this) {
if (this.size < 0 || this.map.isEmpty() && this.size != 0) {//如果沒有分配內(nèi)存空間,拋出異常
throw new IllegalStateException(this.getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
if (this.size <= maxSize || this.map.isEmpty()) {//如果小于內(nèi)存空間,just so so~
return;
}
//否則將使用Lru算法進行移除
Entry<K, V> toEvict = (Entry)this.map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
this.map.remove(key);
this.size -= this.safeSizeOf(key, value);
++this.evictionCount;//回收次數(shù)+1
}
this.entryRemoved(true, key, value, (Object)null);
}
}
這個TrimToSize方法的作用在于判斷內(nèi)存空間是否溢出。利用無限循環(huán),將一個一個的最少使用的數(shù)據(jù)給剔除掉。
get方法 (獲取元素)
@Nullable
public final V get(@NonNull K key) {
if (key == null) {
throw new NullPointerException("key == null");
} else {
Object mapValue;
synchronized(this) {
mapValue = this.map.get(key);
if (mapValue != null) {
++this.hitCount;//命中次數(shù)+1,并且返回mapValue
return mapValue;
}
++this.missCount;//未命中次數(shù)+1
}
/*
如果未命中,會嘗試利用create方法創(chuàng)建對象
create需要自己實現(xiàn),若未實現(xiàn)則返回null
*/
V createdValue = this.create(key);
if (createdValue == null) {
return null;
} else {
synchronized(this) {
//創(chuàng)建了新對象之后,再將其添加進map中,與之前put方法邏輯基本相同
++this.createCount;
mapValue = this.map.put(key, createdValue);
if (mapValue != null) {
this.map.put(key, mapValue);
} else {
this.size += this.safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
this.entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
this.trimToSize(this.maxSize);//每次加入數(shù)據(jù)時,都需要判斷一下是否溢出
return createdValue;
}
}
}
}
create方法 (嘗試創(chuàng)造對象)
@Nullable
protected V create(@NonNull K key) {
return null;//這個方法需要自己實現(xiàn)
}
get方法和create方法的注釋已經(jīng)寫在了代碼上,這里邏輯同樣不是很復(fù)雜。但是我們需要注意的是map的get方法,既然LinkedHashMap能實現(xiàn)Lru算法,那么它的內(nèi)部一定不簡單!
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;
}
LinkedHashMap中,首先進行了判斷,是否找到該元素,沒找到則返回null。找到則調(diào)用afterNodeAccess方法。
LinkedHashMap的afterNodeAccess方法
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMapEntry<K,V> last;
//accessOrder為true 且當前節(jié)點不是尾節(jié)點 則按訪問順序排序
if (accessOrder && (last = tail) != e) {
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
//下面是排序過程
p.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;
}
}
原來如此!LinkedHashMap在這個方法中實現(xiàn)了按訪問順序排序,這也就是為什么我們的LruCache底層是使用的LinkedHashMap作為數(shù)據(jù)結(jié)構(gòu)。
主要方法已經(jīng)講完了 ,接下里我們就看看其他方法吧。
remove (移除元素)
@Nullable
public final V remove(@NonNull K key) {
if (key == null) {//判空
throw new NullPointerException("key == null");
} else {
Object previous;
synchronized(this) {
previous = this.map.remove(key);//根據(jù)key移除value
if (previous != null) {
this.size -= this.safeSizeOf(key, previous);//減掉value的大小
}
}
if (previous != null) {
this.entryRemoved(false, key, previous, (Object)null);
}
return previous;
}
}
evictAll方法(移除所有元素)
public final void evictAll() {
this.trimToSize(-1);//移除掉所有的value
}
其他方法
public final synchronized int size() {
return this.size;//當前內(nèi)存空間的size
}
public final synchronized int maxSize() {
return this.maxSize;//內(nèi)存空間最大的size
}
public final synchronized int hitCount() {
return this.hitCount;//命中個數(shù)
}
public final synchronized int missCount() {
return this.missCount;//未命中個數(shù)
}
public final synchronized int createCount() {
return this.createCount;//創(chuàng)建Value的個數(shù)
}
public final synchronized int putCount() {
return this.putCount;//put進去的個數(shù)
}
public final synchronized int evictionCount() {
return this.evictionCount;//移除個數(shù)
}
public final synchronized Map<K, V> snapshot() {
return new LinkedHashMap(this.map);//創(chuàng)建LinkedHashMap
}
public final synchronized String toString() {//toString
int accesses = this.hitCount + this.missCount;
int hitPercent = accesses != 0 ? 100 * this.hitCount / accesses : 0;
return String.format(Locale.US, "LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", this.maxSize, this.hitCount, this.missCount, hitPercent);
}
最后
有了這篇文章,相信大家對LruCache的工作原理已經(jīng)很清楚了吧!有什么不對的地方希望大家能夠指正。學無止境,大家一起加油吧。