LruCache 源碼剖析

原文發表於:blog.csdn.net/qq_27485935 , 你們沒事能夠去逛逛 (ง •̀_•́)งjavascript

前言

有必定經驗的開發者都知道這個類, 大多數狀況 LruCache 類都被用在圖片緩存這一塊, 而其中使用了一個聽起來高大上的算法 —— 「近期最少使用算法」。 在通過一輪源碼的解析以後, 咱們會發現內部是使用了簡單的技巧來實現的。java

源碼剖析

首先咱們來看一下 LruCache 的構造方法算法

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

能夠看到內部使用 LinkedHashMap 實現。緩存

在通常狀況下, 當須要緩存某個對象時, 調用的是 put 方法ide

LruCache#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 += 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;
}複製代碼

咱們來看一下 ②, 調用了 safeSizeOf 方法, 該方法用來計算 value 佔用大小的。ui

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

/** * Returns the size of the entry for {@code key} and {@code value} in * user-defined units. The default implementation returns 1 so that size * is the number of entries and max size is the maximum number of entries. * * <p>An entry's size must not change while it is in the cache. */
protected int sizeOf(K key, V value) {
    return 1;
}複製代碼

能夠看到 sizeof 方法默認返回1, 因此咱們在使用 LruCache 類時經常須要重寫該方法, 指定 key 和 value 的佔用空間大小。this

再回到 put 方法中, 研究一下③方法, 調用了 map 的 put 方法, map 即爲初始化時候的 LinkedHashMap, 而 LinkedHashMap 繼承了 HashMap 的 put 方法。spa

HashMap#put

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);

    // hash 值與 length -1 做與操做, 至關於取餘, 計算出位標
    int i = indexFor(hash, table.length);  

    // 找到 i 位置hash和key相同的位置, 若是不爲空, 且 hash 值與 key 值相同, 替換舊數值
    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);
    return null;
}複製代碼

要注意 LinkedHashMap 重寫了 HashMap 的 addEntry 方法, 該方法沒處理什麼, 接着看 HashMap 的方法。.net

LinkedHashMap#addEntry

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);
        } finally {
            size--;
        }
        if (removeEldest) {
            removeEntryForKey(eldest.key);
        }
    }

    super.addEntry(hash, key, value, bucketIndex);
}複製代碼

HashMap#addEntry

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length); //擴容到原來的2倍
        hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}複製代碼

能夠看到, 若是添加對象事後的大小大於指定值, 將進行擴容, 在這裏先無論它。 繼續看方法末尾調用的 createEntry 方法。code

HashMap#createEntry

/** * This override differs from addEntry in that it doesn't resize the * table or remove the eldest entry. */
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++;
}

private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        LinkedHashMapEntry<K,V> before, after;

        LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<K,V> next) {
            super(hash, key, value, next);
        }
        ...
}複製代碼

咱們能夠看到, 新增結點除了初始化 LinkedHashMapEntry (實質初始化 HashMapEntry 的內部屬性, 初始化時使用鏈地址法解決衝突) 內的屬性外, 還調用了 LinkedHashMapEntry 對象的 addBefore 方法。

LinkedHashMapEntry#addBefore

private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
    after  = existingEntry;
    before = existingEntry.before;
    before.after = this;
    after.before = this;
}複製代碼

這個 header 又是個啥?

咱們看看 header 對象的初始化過程, 在構造 LinkedHashMap 初始化的過程, (同時父類 HashMap 也初始化, 並調用 init 方法), 調用了 init 方法。

/** * The head of the doubly linked list. 雙向鏈表的頭, 初始化時 header 的前驅後繼都指向本身 */
private transient LinkedHashMapEntry<K,V> header;

@Override
void init() {  
    header = new LinkedHashMapEntry<>(-1, null, null, null);
    header.before = header.after = header;
}複製代碼

header 是默認沒有鍵和值的, 默認前驅和後繼都指向本身。 如圖:

默認結構

因此調用完上面的 addBefore 方法後, 結構是這樣的:

添加第一個結點後

若是添加第二個結點的話, 仍是看 addBefore 方法, 結構是這樣的:

添加第二個結點後

最後讓咱們回到 LruCache 的 put 方法, 看最後一步 ④, trimToSize 是幹嗎的呢?

LruCache#trimToSize

/** * Remove the eldest entries until the total of remaining entries is at or * below the requested size. * * @param maxSize the maximum size of the cache before returning. May be -1 * to evict even 0-sized elements. */
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!");
            }

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

能夠看到官方註釋, 若是請求的空間不夠時, 將移除最近未使用的 entry 鍵值對, 近期最少使用是怎麼判斷的呢。 先獲取存放全部 Entry 的 set 容器, 直接移除 next 方法獲取的 Entry, 移除 entry 直到 size 小於 maxSize, 這個技巧真是 666;

如今來研究一下 entrySet 方法和 iterator 方法和 next 方法。 entrySet 方法 LinkedHashMap 是從 HashMap 繼承過來的

// ---------------- HashMap--------------
public Set<Map.Entry<K,V>> entrySet() {
    return entrySet0();
}

private Set<Map.Entry<K,V>> entrySet0() {
    Set<Map.Entry<K,V>> es = entrySet;
    return es != null ? es : (entrySet = new EntrySet());
}

private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public Iterator<Map.Entry<K,V>> iterator() {
        return newEntryIterator();   // !!!!!!!!! 看這
    }
    ...
}複製代碼

這裏要注意一下這個 newEntryIterator 是調用誰的方法的, 咱們能夠看到 HashMap 和 LinkedHashMap 都有這個方法

// HashMap
Iterator<Map.Entry<K,V>> newEntryIterator()   {
    return new EntryIterator();
}
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {   // ①
    public Map.Entry<K,V> next() {
        return nextEntry();
    }
}

// LinkedHashMap 確定重寫了父類 newEntryIterator 方法
Iterator<Map.Entry<K,V>> newEntryIterator() { 
    return new EntryIterator(); 
}
private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {  // ② 
    public Map.Entry<K,V> next() { return nextEntry(); }
}複製代碼

① 和 ② 明顯就不同, 父類不一樣。 剛開始我研究的時候, 就看錯研究對象了, 搞得最後一臉懵逼。 咱們這裏研究的對象是 LinkedHashMap, 因此調用 next 方法後, 將會調用 LinkedHashIterator 的 nextEntry 方法。

LinkedHashMap 內部類 LinkedHashIterator

private abstract class LinkedHashIterator<T> implements Iterator<T> {
    LinkedHashMapEntry<K,V> nextEntry    = header.after;
    LinkedHashMapEntry<K,V> lastReturned = null;

    ...

    Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (nextEntry == header)
            throw new NoSuchElementException();

        LinkedHashMapEntry<K,V> e = lastReturned = nextEntry;
        nextEntry = e.after;
        return e;
    }
}複製代碼

第一次進行 Iterator 遍歷時, 最早獲得的便是 header.after 指向的對象, 結合上面的 trimToSize 方法, 能夠發現, 第一次 next 獲得的對象, map 對其直接做 remove 處理。 厲害了, 那就說明 header.after 指向的對象就是最近最少使用對象。

那, 若是我經過 get 方法, 取出對象使用, LinkedHashMap 的內部結構又會有什麼變化呢。

因此咱們看看, LinkedHashMap 的 get 方法。

LinkedHashMap#get

public V get(Object key) {
    // ①
    LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key); 
    if (e == null)
        return null;
    // ②
    e.recordAccess(this);
    return e.value;
}複製代碼

我們先看看 ① 作了什麼。

HashMap#getEntry

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
    for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}複製代碼

這個方法沒什麼特殊的, 找到 hash 對應位標, 再找到 hash 值與 key 值相同的對象, 最後依次對象返回。

咱們回到 get 方法, 看 ② 發生了什麼, 這個方法更厲害了!!

LinkedHashMapEntry#recordAccess

void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    if (lm.accessOrder) {  // accessOrder 在 LruCache 初始化 LinkedHashMap 過程當中置爲 true 了
        lm.modCount++;
        remove();  // 
        addBefore(lm.header);
    }
}複製代碼

先看 remove 方法。

private void remove() {
    before.after = after;
    after.before = before;
}複製代碼

調用完 remove 後, 結構如圖:(假設咱們要 get 的是圖中 ① 對象)

調用完 remove

如今看看 addBefore 方法

private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
    after  = existingEntry;
    before = existingEntry.before;
    before.after = this;
    after.before = this;
}複製代碼

調用完 addBefore 後, 最終的結構如圖:

調用完 addBefore

因此當經過 get 方法 「使用」 了一個對象, 該對象將會放在鏈表最末端, 因此近期最少使用的對象也就是 header.after 指向的對象了。

總結

相關文章
相關標籤/搜索