LruCache 源碼剖析

前言

有一定經(jīng)驗的開發(fā)者都知道這個類乳绕, 大多數(shù)情況 LruCache 類都被用在圖片緩存這一塊, 而其中使用了一個聽起來高大上的算法 —— “近期最少使用算法”礼烈。 在經(jīng)過一輪源碼的解析之后, 我們會發(fā)現(xiàn)內部是使用了簡單的技巧來實現(xiàn)的瞻润。

源碼剖析

首先我們來看一下 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 實現(xiàn)版扩。

在一般情況下废离, 當需要緩存某個對象時, 調用的是 put 方法

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 占用大小的。

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 的占用空間大小。

再回到 put 方法中窄刘, 研究一下③方法, 調用了 map 的 put 方法舷胜, map 即為初始化時候的 LinkedHashMap娩践, 而 LinkedHashMap 繼承了 HashMap 的 put 方法。

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 值相同吨岭, 替換舊數(shù)值
    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 的方法辣辫。

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

可以看到, 如果添加對象過后的大小大于指定值魁巩, 將進行擴容急灭, 在這里先不管它。 繼續(xù)看方法末尾調用的 createEntry 方法谷遂。

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;

現(xiàn)在來研究一下 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 方法笋颤, 可以發(fā)現(xiàn)乳附, 第一次 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 方法今魔, 看 ② 發(fā)生了什么, 這個方法更厲害了U厦场错森!

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

現(xiàn)在看看 addBefore 方法

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

調用完 addBefore 后涩维, 最終的結構如圖:

調用完 addBefore

所以當通過 get 方法 “使用” 了一個對象, 該對象將會放在鏈表最末端嘀粱, 所以近期最少使用的對象也就是 header.after 指向的對象了激挪。

總結

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锋叨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宛篇,老刑警劉巖娃磺,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異叫倍,居然都是意外死亡偷卧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進店門吆倦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來听诸,“玉大人,你說我怎么就攤上這事蚕泽∩卫妫” “怎么了?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵须妻,是天一觀的道長仔蝌。 經(jīng)常有香客問我,道長荒吏,這世上最難降的妖魔是什么敛惊? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮绰更,結果婚禮上瞧挤,老公的妹妹穿的比我還像新娘锡宋。我一直安慰自己,他們只是感情好特恬,可當我...
    茶點故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布执俩。 她就那樣靜靜地躺著,像睡著了一般鸵鸥。 火紅的嫁衣襯著肌膚如雪奠滑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天妒穴,我揣著相機與錄音宋税,去河邊找鬼。 笑死讼油,一個胖子當著我的面吹牛杰赛,可吹牛的內容都是我干的。 我是一名探鬼主播矮台,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼乏屯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瘦赫?” 一聲冷哼從身側響起辰晕,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎确虱,沒想到半個月后含友,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡校辩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年窘问,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宜咒。...
    茶點故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡惠赫,死狀恐怖,靈堂內的尸體忽然破棺而出故黑,到底是詐尸還是另有隱情儿咱,我是刑警寧澤,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布倍阐,位于F島的核電站概疆,受9級特大地震影響,放射性物質發(fā)生泄漏峰搪。R本人自食惡果不足惜岔冀,卻給世界環(huán)境...
    茶點故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧使套,春花似錦罐呼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至奉呛,卻和暖如春计螺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瞧壮。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工登馒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咆槽。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓陈轿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親秦忿。 傳聞我的和親對象是個殘疾皇子麦射,可洞房花燭夜當晚...
    茶點故事閱讀 45,446評論 2 359

推薦閱讀更多精彩內容

  • 非原創(chuàng),尊重作者灯谣!官方鏈接:http://www.reibang.com/p/b49a111147ee 關于And...
    小小程序員jh閱讀 1,842評論 0 16
  • LruCache源碼解析 LruCache是Android中的一個緩存工具類潜秋,它采用了一種最近最少使用算法,可以將...
    RainMi閱讀 402評論 0 6
  • 1. Java基礎部分 基礎部分的順序:基本語法胎许,類相關的語法半等,內部類的語法,繼承相關的語法呐萨,異常的語法,線程的語...
    子非魚_t_閱讀 31,660評論 18 399
  • 作為一枚一線用戶研究員莽囤,雖然工作時間不長谬擦,但掉的坑多呀:報告發(fā)出去如石沉大海,意見提出來產(chǎn)品經(jīng)理杳無回應朽缎,項目參與...
    喬溪閱讀 1,585評論 0 6
  • 雙調·北林 短相聚惨远,長別離,久辭北土南歸去话肖。 二三雨落舊容乍起北秽,只得愿君入夢里。
    景卓慧閱讀 185評論 0 0