Android LruCache源碼分析

Android設備因爲內存大小限制,對於一些資源的緩存不能無限的添加,須要限制它的大小,那麼當它達到最大限制時,如何選擇要被移除出去的對象呢,Android sdk中爲咱們提供了一個LruCache的類,它可讓咱們很方便的管理緩存。java

概述

它是一個持有有限數量的值的緩存池,當一個值被訪問到了,它就會被移動到隊列的頭部,當一個值被添加到一個裝滿了的緩存池,那麼隊列尾部的值將會被剔除出去而且能夠被垃圾回收期回收。node

這個類不容許null做爲key或者value,若是get,put,remove方法返回了一個null,那就很明確的代表了這個key不存在。緩存

這個類其實很簡單,只有三百多行代碼,它內部由一個LinkedHashMap來實現數據存儲,其主要邏輯也是在LinkedHashMap中實現的,那麼咱們先來看一下LinkedHashMap。併發

LinkedHashMap

LinkedHashMap也是一個比較簡單的類,它繼承自HashMap,數據存儲也跟HashMap一致,可是它內部維護了一個雙向鏈表來記錄全部節點,下面來看源碼:app

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
	...
}
複製代碼

put

咱們發現它並無重寫put方法, 那麼它是如何將node節點鏈接起來的呢,還須要在HashMap的put中找線索。ide

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        //建立新節點
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    //建立新節點
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
    ...
        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            //建立新的樹節點
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    ...
}
複製代碼

咱們能夠發現put過程當中調用了建立節點的方法,而LinkHashMap中重寫了這兩個方法this

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMapEntry<K,V> p =
        new LinkedHashMapEntry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}

TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    linkNodeLast(p);
    return p;
}

//將p放入鏈表尾部
private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
    LinkedHashMapEntry<K,V> last = tail;
    tail = p;//tail賦值爲p
    //說明是第一個元素
    if (last == null)
        head = p;
    else {//將p的與以前的tail連接
        p.before = last;
        last.after = p;
    }
}
複製代碼

咱們發如今建立節點的時候將p插入鏈表的尾部,其中head與tail是兩個全局變量,表示鏈表的頭節點與尾節點,咱們還注意到HashMap的put方法最後一步執行了afterNodeInsertion()方法,這個方法在HashMap中是空實現,可是在LinkedHashMap中實現了spa

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMapEntry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}
複製代碼

方法中移除了鏈表中的第一個元素,可是removeEldestEntry在這裏默認返回的null,也就是這裏不作處理,若是你想在添加一個元素後,移除頭部的元素,只須要實現removeEldestEntry方法,在知足移除條件的時候返回true便可,一般用於緩存。線程

get

LinkedHashMap重寫了get方法,它與HashMap惟一的不一樣就是當accessOrder爲true是調用了afterNodeAccess方法。code

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

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMapEntry<K,V> last;
    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;
    }
}
複製代碼

而afterNodeAccess方法就是把當前獲取的節點放到隊列的尾部。

remove

LinkedHashMap一樣沒有重寫該方法,可是它實現了afterNodeRemoval,而HashMap的removeNode的方法最後也會調用這個方法

void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMapEntry<K,V> p =
        (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}
複製代碼

這個方法就是講當前要移除的節點斷開與鏈表的連接。

LruCache

下面開始看LruCache

/** * @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);
}
複製代碼

LruCache構造方法接收一個maxSize做爲緩存的最大值的參數,若是咱們沒有重寫sizeOf方法,那麼它就表明元素的個數。

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
        size += safeSizeOf(key, value);
        //放入集合
        previous = map.put(key, value);
        if (previous != null) {
            //若是previous不爲null,表示只是更新了值,此時還須要減去previous的大小
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }
	//檢查size是否超出
    trimToSize(maxSize);
    return previous;
}

private int safeSizeOf(K key, V value) {
    int result = sizeOf(key, value);
    if (result < 0) {
        throw new IllegalStateException("Negative size: " + key + "=" + value);
    }
    return result;
}

protected int sizeOf(K key, V value) {
    return 1;
}

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沒有超出,跳出循環
            if (size <= maxSize) {
                break;
            }
            //不然,找到map中最老的元素,返回的是鏈表的頭部
            Map.Entry<K, V> toEvict = map.eldest();
            if (toEvict == null) {
                break;
            }
            key = toEvict.getKey();
            value = toEvict.getValue();
            //移除
            map.remove(key);
            //更新size
            size -= safeSizeOf(key, value);
            evictionCount++;
        }
        entryRemoved(true, key, value, null);
    }
}

LinkedHashMap:
public Map.Entry<K, V> eldest() {
    return head;
}
複製代碼

put時若是原來key對應的value不爲空,調用了entryRemoved,這裏是個空實現,若是咱們保存的資源須要手動來釋放的話能夠重寫該方法來釋放資源,還有sizeOf默認返回1,也就是元素的個數,若是咱們但願限制的元素佔用內存的大小,好比Bitmap限制爲4M,那就應該這樣寫,sizeOf返回bitmap的大小作累加,entryRemoved釋放bitmap

int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
     protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount();
     }
    
    protected void entryRemoved(boolean evicted, K key, Bitmap oldValue, Bitmap newValue) {
        oldValue.recycle();
    }
}}
複製代碼

get

經過put方法的分析,get就比較好理解了

public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        //正常返回,注意這裏調用了LinkedHashMap的get方法後,將當前節點插入到鏈表的尾部
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }
	//若是沒有key,則建立一個key對應的value,默認實現是null
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    synchronized (this) {
        createCount++;
        //將新建立的值放入map
        mapValue = map.put(key, createdValue);
        //正常狀況下應該不會出現這種狀況,應該是在併發時,若是一個線程create時另一個線程進入該同步塊put元素後會出現這種狀況
        if (mapValue != null) {
            // There was a conflict so undo that last put
            map.put(key, mapValue);
        } else {
            //size增長,safeSizeOf調用了sizeOf,sizeOf默認返回1,也就是元素的數量
            size += safeSizeOf(key, createdValue);
        }
    }
	//若是maoValuw不爲null,size沒有發生變化,直接返回
    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {//不然檢查size是否超過最大容許的大小
        trimToSize(maxSize);
        return createdValue;
    }
}

複製代碼

remove

remove也比較簡單,不過多介紹

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(false, key, previous, null);
    }

    return previous;
}
複製代碼

總結

LruCache的大體流程,當put一個鍵值對的時候,經過LinkedHashMap將該節點放到鏈表的末端,當get一個key的時候,若是存在,則將該節點插入鏈表末端,而當size超出maxSize時,刪除頭部節點,也就是說當一個key對應的節點頻繁被訪問,或者剛剛被訪問過,那麼它必定是在鏈表的後面的,因此刪除的時候就不會刪除這個節點,而若是一個key對用的節點很長時間不訪問到時它在頭部,那麼一旦size超限,它就會被刪除,這就是LRU的實現方式:最近最少使用。

因此源碼中對類的描述:每當一個value被訪問時,它就會被移動到隊列的頭部,這個說法的方向應該是跟LinkedHashMap中雙向鏈表的方向是相反的。

相關文章
相關標籤/搜索