1. 程式人生 > >Android LruCache原始碼分析,圖片快取演算法的實現原理

Android LruCache原始碼分析,圖片快取演算法的實現原理

640?wx_fmt=png&wxfrom=5&wx_lazy=1

今日科技快訊

昨日,記者從中國聯通處獨家獲悉,聯通計劃在北京、天津、上海、深圳、杭州、南京、雄安7城市進行5G試驗,已經向工信部遞交了申請。並表示,未來還會根據工信部、發改委的要求,增加試驗城市的數量。

作者簡介

本篇文章來自 楓葉棧 的投稿。主要介紹了Android中LruCache原始碼相關知識,希望對大家有所幫助!

楓葉棧 的部落格地址

https://www.jianshu.com/u/dfd117c5b6de

LruCache概要

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)); 
    } 
}

測試截圖如下:

0?wx_fmt=png

當我們訪問連結串列中一個元素時候,該元素會移到佇列末尾,測試如下:

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); 
}

測試截圖如下:

0?wx_fmt=png

我們發現第一個元素被移除。我們會發現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); 
}

測試截圖如下:

0?wx_fmt=png

我們發現超出總大小後,佇列首部元素被移除。移除相對簡單,不會改變佇列裡元素的相對順序,只是該元素出佇列而已,測試如下:

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的元素被移除連結串列,結果如下:

0?wx_fmt=png

總結

讀完原始碼我們可以總結出:

  1. LruCache是繼承自HashMap,它的查詢效率依然是O(1);

  2. LruCache內部又維護了一個雙向連結串列結構,當我們有訪問操作時候,被訪問節點會移到連結串列前邊

  3. 在Lrucache的put、get和remove方法中,對集合操作時使用了synchronized關鍵字,來保證執行緒安全;

OK,關於LruCache原始碼實現層面的就分析完了,感謝你的耐心閱讀。

歡迎長按下圖 -> 識別圖中二維碼

或者 掃一掃 關注我的公眾號

640.png?

640?wx_fmt=jpeg