簡介
我們在做圖片三級緩存時,內存緩存為了防止內存溢出,導致APP崩潰,使用LruCache<K, V>來管理內存數據,內部由最近最少使用算法實現,將內存控制在一定的大小內,超出最大值時會自動回收。
原理
初始化時指定最大內存大小,google原生OS的默認值是16M,但是各個廠家的OS會對這個值進行修改,有的128 有的256等等。可使用val maxMemory = Runtime.getRuntime().maxMemory()來動態獲取手機的內存大小,一般控制在總內存的1/8。
初始化
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);
}
new LruCache<String,Bitmap>((int) maxMemory/8)
添加數據
當有新數據添加時,往LinkedHashMap里面添加數據存儲到鏈表尾端,如果之前存在則移除之前的數據,添加完成之后計算是否超過最大內存。
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;
}
判斷內存集合是否超過最大限制;如果超過,將鏈表頭部的對象也就是近期最少用到的數據移除,直到內存大小滿足最大初始值。
private 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 = null;
for (Map.Entry<K, V> entry : map.entrySet()) {
toEvict = entry;
}
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
獲取數據
獲取數據時,有數據時,直接返回數據,沒有數據就調用create返回自定義的或者null
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
初始化LinkedHashMap accessOrder傳值為true,調用afterNodeAccess;
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;
}
當accessOrder的值為true,且e不是尾節點,將e移到鏈表的尾端;
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<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;
}
}
LruCache 局部同步鎖
在 get, put, trimToSize, remove 四個方法里的 entryRemoved 方法都不在同步塊里,因為 entryRemoved 回調的參數都屬于方法域參數,不會線程不安全。
總結
通過以上源碼的分析,可以知道新增數據和獲取緩存時,數據都會移動到鏈表尾端,而當前內存大于設置最大內存的大小時,會移除鏈表頭端的數據,與hitCount++的數量毫無關系。
面試題:最近最少使用算法是使用次數多的還是最近的數據先被移除?