Android LruCache原始碼分析,圖片快取演算法的實現原理
昨日,記者從中國聯通處獨家獲悉,聯通計劃在北京、天津、上海、深圳、杭州、南京、雄安7城市進行5G試驗,已經向工信部遞交了申請。並表示,未來還會根據工信部、發改委的要求,增加試驗城市的數量。
作者簡介本篇文章來自 楓葉棧 的投稿。主要介紹了Android中LruCache原始碼相關知識,希望對大家有所幫助!
楓葉棧 的部落格地址:
LruCache概要https://www.jianshu.com/u/dfd117c5b6de
LRU (Least Recently Used) 即最近最少使用演算法。在Android開發中,LruCache是基於LRU演算法實現的。當快取空間使用完的情況下,最久沒被使用的物件會被清除出快取。
LruCache常用的場景是做圖片記憶體快取,電商類APP經常會用到圖片,當我們對圖片資源做了記憶體快取,不僅可以增強使用者體驗,而且可以減少圖片網路請求,減少使用者流量耗費。
LruCache是一個記憶體層面的快取,如果想要進行本地磁碟快取,推薦使用DiskLruCache,雖然沒包含在官方API中,但是官方推薦我們使用。
使用方法LruCache的使用方法如下:
public class BitmapLruCache extends LruCache<String, Bitmap> {
//設定快取大小,建議當前應用可用最大記憶體的八分之一 即(int) (Runtime.getRuntime().maxMemory() / 1024 / 8)
public BitmapLruCache(int size) {
super(size);
}
//計算當前節點的記憶體大小 這個方法需要重寫 不然返回1
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount() / 1024;
}
//當節點移除時該方法會回撥,可根據需求來決定是否重寫該方法
@Override
protected void entryRemoved (boolean evicted, String key, Bitmap oldValue, Bitmap
newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
}
}
往快取中放一張圖片:
mLruCache.put(name, bitmap);
獲取一張圖片:
mLruCache.get(name);
刪除一張圖片:
mLruCache.remove(name);
原始碼分析首先先看一下LruCache構造方法
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
//我們發現實現用的是LinkedHashMap 注意最後的true 表示LinkedHashMap 中accessOrder設定為true
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
接著,我們繼續來看下put方法
/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
public final V put(K key, V value) {
if (key == null || value == null) {//不允許key 和 value 為 null
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {//多執行緒安全
putCount++;
//size 表示當期使用的快取大小 safeSizeOf 會掉用sizeOf方法 用於計算當前節點的大小
size += safeSizeOf(key, value);
// 將新節點放入LinkedHashMap 如果有返回值,表示map集合中存在舊值
previous = map.put(key, value);
if (previous != null) {//存在舊值 在移除舊值後 更新快取大小
size -= safeSizeOf(key, previous);
}
}
//有舊值移除 回撥entryRemoved
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);//整理每個節點 主要判斷當前size是否超過maxSize
return previous;
}
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 || 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);
}
}
再來看下safeSizeOf 和 entryRemoved方法
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);//呼叫了sizeOf方法
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
protected int sizeOf(K key, V value) {//sizeOf的預設實現
return 1;
}
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}//entryRemoved的預設實現
get方法
/**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
* be created.
*/
public final V get(K key) {
if (key == null) {//key不能為 null
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {//執行緒同步
mapValue = map.get(key);//呼叫LinkedHashMap 的get方法
if (mapValue != null) {
hitCount++;
return mapValue;//找到返回
}
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.
*/
V createdValue = create(key);//快取沒有情況下走建立流程
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);
if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}
if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
protected V create(K key) {//建立方法預設空實現 可根據需求決定是否重寫該方法
return null;
}
最後,我們來看下remove方法
/**
* Removes the entry for {@code key} if it exists.
*
* @return the previous value mapped by {@code 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方法
entryRemoved(false, key, previous, null);
}
return previous;
}
通過上述幾個方法程式碼,我們知道LruCache如何控制及更新快取的大小的,主要是線上程同步塊裡對size欄位進行更新,然後根據size欄位和maxSize欄位的大小關係來修剪節點。但如何做到最近最少使用呢? 沒錯,LinkedHashMap 幫我們做到最近最少使用的排序。
讓我們看下LinkedHashMap 如何實現的,在此過程我們不分析HashMap的實現,只關心LinkedHashMap 的一些實現,HashMap的實現有機會給大家分享。
//繼承了HashMap 說明LinkedHashMap 的查詢效率依然是O(1)
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
//重新定義了節點 用於實現連結串列
private transient LinkedHashMapEntry<K,V> header;
private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
LinkedHashMapEntry<K,V> before, after;
//此處省略幾個字
}
//此處省略幾個字
}
LinkedHashMap類並沒有重寫put方法,當我們呼叫put方法時,呼叫的依然是HashMap的put方法。我們看下HashMap的put方法:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
//HashMap的特點 可以放key為null的值
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;//存在舊值 就把舊值移除掉 並返回舊值 由此可知 當我們更換快取中已存在的值時,並不會影響它在連結串列中位置
}
}
modCount++;
addEntry(hash, key, value, i);//LinkedHashMap 重寫了該方法
return null;
}
//LinkedHashMap中實現
void addEntry(int hash, K key, V value, int bucketIndex) {
// Previous Android releases called removeEldestEntry() before actually
// inserting a value but after increasing the size.
// The RI is documented to call it afterwards.
// **** THIS CHANGE WILL BE REVERTED IN A FUTURE ANDROID RELEASE ****
// Remove eldest entry if instructed
LinkedHashMapEntry<K,V> eldest = header.after;
if (eldest != header) {
boolean removeEldest;
size++;
try {
removeEldest = removeEldestEntry(eldest);//hook 預設為false 讓使用者重寫removeEldestEntry 來決定是否移除eldest節點
} finally {
size--;
}
if (removeEldest) {
removeEntryForKey(eldest.key);
}
}
super.addEntry(hash, key, value, bucketIndex);//往下瞅 有驚喜
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
//HashMap中addEntry:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//LinkedHashMap重寫了該方法
createEntry(hash, key, value, bucketIndex);
}
//LinkedHashMap中
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++;
}
LinkedHashMap中重寫了get方法,實現如下;
public V get(Object key) {
//使用HashMap的getEntry方法,驗證了我們所說的查詢效率為O(1)
LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);//在找到節點後呼叫節點recordAccess方法 往下看
return e.value;
}
//LinkedHashMapEntry
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {//accessOrder該值預設為false 但是 在LruCache中設定為true
lm.modCount++;
remove();//該方法將本身節點移除連結串列
addBefore(lm.header);//將自己新增到節點頭部 保證最近使用的節點位於連結串列前邊
}
}
LinkedHashMap的並沒有重寫HashMap的remove方法,依然是呼叫HashMap的remove方法,程式碼如下:
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.getValue());
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
HashMapEntry<K,V> prev = table[i];
HashMapEntry<K,V> e = prev;
while (e != null) {
HashMapEntry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
//注意這一行 開頭我們說過 LinkedHashMap用的節點是自己定義的LinkedHashMapEntry ,繼承自HashMapEntry
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
//LinkedHashMapEntry 中recordRemoval
void recordRemoval(HashMap<K,V> m) {
remove();
}
private void remove() {//更改指標來移除自身
before.after = after;
after.before = before;
}
demo驗證首先建立LRUCache,我們設定其大小為4,因為預設每個Item大小為1,當我們執行放入操作時,最後放入的資料會在連結串列最後邊,列印結果如下:
private void test() {
LruCache<String,String> lruCache = new LruCache<>(4);
lruCache.put("a","這是第1個放進去的值");
lruCache.put("b","這是第2個放進去的值");
lruCache.put("c","這是第3個放進去的值");
lruCache.put("d","這是第4個放進去的值");
print(lruCache);
}
private void print(LruCache<String,String> lruCache){
Map map =lruCache.snapshot();
Iterator<String> iterator = map.keySet().iterator();
String key;
while (iterator.hasNext()){
key = iterator.next();
Log.i("LruCacheItem","key ="+key+" value ="+map.get(key));
}
}
測試截圖如下:
當我們訪問連結串列中一個元素時候,該元素會移到佇列末尾,測試如下:
private void test() {
LruCache<String,String> lruCache = new LruCache<>(4);
lruCache.put("a","這是第1個放進去的值");
lruCache.put("b","這是第2個放進去的值");
lruCache.put("c","這是第3個放進去的值");
lruCache.put("d","這是第4個放進去的值");
lruCache.get("c");
print(lruCache);
}
測試截圖如下:
我們發現第一個元素被移除。我們會發現key為c的item移到了佇列末尾。當我們放入元素超過總的大小時,佇列首部元素會被移除:
private void test() {
LruCache<String,String> lruCache = new LruCache<>(4);
lruCache.put("a","這是第1個放進去的值");
lruCache.put("b","這是第2個放進去的值");
lruCache.put("c","這是第3個放進去的值");
lruCache.put("d","這是第4個放進去的值");
lruCache.get("c");
lruCache.put("e","這是第5個放進去的值");
print(lruCache);
}
測試截圖如下:
我們發現超出總大小後,佇列首部元素被移除。移除相對簡單,不會改變佇列裡元素的相對順序,只是該元素出佇列而已,測試如下:
private void test() {
LruCache<String,String> lruCache = new LruCache<>(4);
lruCache.put("a","這是第1個放進去的值");
lruCache.put("b","這是第2個放進去的值");
lruCache.put("c","這是第3個放進去的值");
lruCache.put("d","這是第4個放進去的值");
lruCache.get("c");
lruCache.put("e","這是第5個放進去的值");
lruCache.remove("c");
print(lruCache);
}
key為c的元素被移除連結串列,結果如下:
總結讀完原始碼我們可以總結出:
LruCache是繼承自HashMap,它的查詢效率依然是O(1);
LruCache內部又維護了一個雙向連結串列結構,當我們有訪問操作時候,被訪問節點會移到連結串列前邊
在Lrucache的put、get和remove方法中,對集合操作時使用了synchronized關鍵字,來保證執行緒安全;
OK,關於LruCache原始碼實現層面的就分析完了,感謝你的耐心閱讀。
歡迎長按下圖 -> 識別圖中二維碼
或者 掃一掃 關注我的公眾號