LruCache原理和源碼分析(一)

一、Lru算法

Lru算法:最近最少使用算法账蓉;

算法的核心關鍵類是LinkedHashMap皮璧。

基本算法:將key-value鍵值對按照訪問順序進行排列放置疑俭,當存入的數(shù)據(jù)超過最大內存分配時,移除最久訪問的數(shù)據(jù)在孝;

所以要搞清楚LruCache的原理诚啃,首先需要研究LinkedHashMap這個類;

LinkedHashMap繼承自HashMap私沮,自然也就繼承了HashMap中的眾多方法,所以在研究LinkedHashMap之前和橙,有必要去研究下LinkedHashMap的父類HashMap;

二仔燕、Map接口

Map接口,key-value集合的抽象魔招,反映了單個的key映射著單個的value這樣一種數(shù)據(jù)結構晰搀;

它的核心行為是
存入key-value,對應著put(key,value)方法和根據(jù)key來取出value办斑,對應著get(key);
然后為了功能上的完善外恕,還提供了其他的接口:

//清除map中的所有數(shù)據(jù)
clear();
//是否包含某個key
containsKey(key);
//是否包含某個value
containsValue(value);
//map是否為空
isEmpty()
//根據(jù)某個key來移除key-values
remove(key)

三、HashMap——Map的實現(xiàn)乡翅;

3.1. HashMapEntry

它是HashMap中的一個內部類鳞疲,它是HashMap存儲的數(shù)據(jù)單元,HashMap中的存儲數(shù)組的存儲單位就是HashMapEntry

        final K key;
        V value;
        final int hash;
        HashMapEntry<K, V> next;

主要有四個值蠕蚜,key尚洽,values,hash和next靶累,這個next是為了解決hash沖突而設置的腺毫,當存在hash沖突時,next會鏈式地指向下一個HashMapEntry對象挣柬;這個具有相同table index的entry可以鏈式存儲到table中的同一個位置潮酒;

廢話不多說了,直接進入到HashMap的核心方法get(key)和put(key,value)方法邪蛔;

3.2. put(key,value)方法

    public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }
        //根據(jù)key來計算hash值急黎,至于為什么是Collections.secondaryHash(key)?
        //這樣做只是為了使得hash分布更均勻店溢,避免hash沖突叁熔;
        //具體來參考大神的解釋:http://burtleburtle.net/bob/hash/integer.html
        int hash = Collections.secondaryHash(key);
        //這里就是要存儲數(shù)據(jù)的數(shù)組,基本元素是HashMapEntry
        HashMapEntry<K, V>[] tab = table;
        //根據(jù)hash值和table的長度來計算索引index值床牧,注意一點荣回,計算的index值不能超過數(shù)組的索引范圍
        int index = hash & (tab.length - 1);
        //這一步,需要自己品味和分析戈咳,首先拋出一個問題:如果兩個不同的key心软,計算得到的index恰巧相同壕吹,這種情況下怎么辦呢?
        //上面的問題删铃,其實就是發(fā)生了hash沖突如何解決的問題耳贬,HashMap采用的方法是,當index值相同時猎唁,采用鏈式的存儲方式來存儲不同的key-value值
        //這么說不明白咒劲?不要著急,看下面代碼诫隅,且聽我來細細道來(解釋廢話比較多腐魂,放在代碼后邊,文字里敘述逐纬,免得注釋過多):
        
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);
        return null;
    }

首先處理key為null的情況蛔屹,根據(jù)成員屬性entryForNullKey是否為null,如果為null豁生,則調用addNewEntryForNullKey(value)方法來對其進行賦值:

    void addNewEntryForNullKey(V value) {
        entryForNullKey = new HashMapEntry<K, V>(null, value, 0, null);
    }

這種情況不做過多的論述兔毒;

接下來,是重點哈哈哈哈~~~好像重點有點畫多了哈哈哈甸箱,不過下面是重點中的重點

HashMap是通過基礎的數(shù)據(jù)類型數(shù)組HashMapEntry[] table來存入key-value鍵值對的育叁;
what?摇肌?沒有搞錯把擂红?搞了半天,HashMap是用數(shù)組來實現(xiàn)數(shù)據(jù)存儲的围小?昵骤?數(shù)組的使用我想大家都沒有問題把》》》

當我們往HashMap中存入鍵值對的時候,會根據(jù)key來計算出一個int類型的hash值肯适,然后再將這個hash值轉換成table長度范圍內的索引index变秦,再將value賦值給table[index],經過這樣一個過程,就實現(xiàn)了key-value到數(shù)組存儲的轉化框舔;

兩個不同的key蹦玫,計算得到index值一樣時,也就是存在hash沖突時刘绣,HashMap是如何處理的樱溉?

第一次得到index值時,此時的table[index]為null纬凤,所以直接存入addNewEntry(key, value, hash, index)福贞;
第二次得到index值時,此時的table[index]不為null停士,進入for循環(huán)挖帘,判斷是不是相同的key完丽,如果是,則存入newValue拇舀,返回oldValue逻族;
如果不是相同的key,for循環(huán)中if條件為false骄崩,走addNewEntry(key, value, hash, index)聘鳞;

    //我們仔細回顧下,在執(zhí)行這個方法之前要拂,table[index]已經存入了第一次得到index時的hashMapEntryOne搁痛,
    //那么new HashMapEntry<K, V>(key, value, hash, table[index]),生成新的對象hashMapEntryTwo,它的內部成員屬性next指向了hashMapEntryOne宇弛;
    //結果就是table[index]存入了hashMapEntryTwo的引用,然后hashMapEntryTwo的next又指向了hashMapEntryOne源请;
    //這樣存在hash沖突的兩個key-values鍵值對枪芒,就以鏈式數(shù)據(jù)結構,存儲到了table[index]下谁尸;
    
    void addNewEntry(K key, V value, int hash, int index) {
        table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
    }
    
    HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
    }

3.3. get(key)方法

    public V get(Object key) {
    //首先處理key為null的情況舅踪,根據(jù)成員屬性entryForNullKey的值來返回null或者entryForNullKey的value;
        if (key == null) {
            HashMapEntry<K, V> e = entryForNullKey;
            return e == null ? null : e.value;
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                return e.value;
            }
        }
        return null;
    }

當根據(jù)key來獲取對應的value時良蛮,其實是上述過程的逆過程:
首先根據(jù)key的值來按照特定的算法計算出對應的hash值抽碌,然后將hash值轉換成數(shù)組table的索引index,然后取出table[index]中存儲的值hashMapEntry决瞳,依次去遍歷hashMapEntry中的值(鏈式存儲货徙,當存在hash沖突時,可能會有多個值,依次從next中取出值),在if條件判斷中比較hashMapEntry的key是否與參數(shù)key相同廷没,相同 的話幸乒,說明當前的hashMapEntry的value是我們要取的值;
否則的話硅急,返回null;

整個HashMap的核心就是將key映射到內部存儲結構table的index的方法+HashMapEntry鏈式數(shù)據(jù)結構來解決hash沖突

四、幾個關鍵問題

4.1. 如何避免和解決hash沖突的

HashMap解決hash沖突的方法:
hash復用泻仙,同一個hash值,鏈式地加入多個value量没;
即如果不同的key玉转,計算出的table index相同的話,那么會將這些值存入table[index]中允蜈,而table[index]的值HashMapEntry為鏈式存儲結構冤吨,可以鏈式的的放入多個值蒿柳;

4.2.如何解決HashMap容量不足的問題

前面解決了hash沖突的問題,那么現(xiàn)在如何解決容量不足的問題了漩蟆。

考慮這樣的情況垒探,如果用戶大量地調用put方法,在這種情況下怠李,如果容量不變圾叼,那么勢必會出現(xiàn)大量的沖突,調用get方法時捺癞,可能需要很長長度的遍歷才能得到答案夷蚊,性能損失嚴重,因此髓介,我們可以發(fā)現(xiàn)put(key,value)源碼中有這樣一種方法doubleCapacity(),這個方法在HashMap存儲容量到達閾值時調用惕鼓,使得HashMap的存儲容量增加一倍;

    if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
    }

這個方法的具體原理唐础,有興趣的可以去看下源碼箱歧,這里我只想說一下,擴容之后一膨,舊數(shù)組和新數(shù)組的index之前的關系:

            int highBit = e.hash & oldCapacity;
            HashMapEntry<K, V> broken = null;
            newTable[j | highBit] = e;

oldCapacity假設為16(00010000), int highBit = e.hash & oldCapacity 能夠得到高位的值呀邢,因為低位全為0,經過與操作過后豹绪,低位一定是0价淌。J 在這里是index,J 與 高位的值進行‘|’操作過后瞒津,就能得到在擴容后面的新的index值蝉衣。
設想一下,理論上我們得到的新的值應該是 newValue = hash & (newCapacity - 1) 仲智,與 oldValue = hash & (oldCapacity - 1) 的區(qū)別僅在于高位上买乃。 因此我們用 J | highBit 就可以得到新的index值。

4.3.如何解決迭代問題的

4.4.HashMap 是如何實現(xiàn)序列化接口的

這兩個問題钓辆,可以參考下面這篇文章:
http://www.woaitqs.cc/program/2015/04/14/read-source-code-about-hashmap

五剪验、LinkedHashMap——有序的Map接口實現(xiàn);

5.1

有序指的是元素可以按照插入順序或者訪問順序排列前联;

既然有兩種順序選擇功戚,就需要一個標志位來確定應用中選擇哪種排序方式;

LinkedHashMap成員屬性boolean accessOrder就是這樣一個標志位:

如果為true的話似嗤,LinkedHashMap應用訪問順序排序啸臀;

如果為false的話,LinkedHashMap應用插入順序排序;

5.2.

LinkedHashMap內部包含一個雙向循環(huán)鏈表乘粒,元素排序就是根據(jù)這個雙向循環(huán)鏈表來進行的豌注;

其數(shù)據(jù)結構示意圖如下:


image

雙向循環(huán)鏈表示意圖:


image

雙向循環(huán)鏈表頭部存儲的是最久訪問或者最先插入的節(jié)點;
尾部存放的是最近訪問或者最近插入的節(jié)點灯萍;

迭代器的遍歷方向是從鏈表的頭部開始到鏈表的尾部結束轧铁;

鏈表的頭部(也可以叫尾部,叫法不一樣旦棉,但不影響它的本質)有一個空的節(jié)點header齿风;

該節(jié)點不存放key-value鍵值,是LinkedHashMap類的成員屬性绑洛,是雙向循環(huán)鏈表的入口救斑;

5.3.LinkedHashMap中的雙向鏈表的操作

示意圖


image

說了這么多,都是描述性的語言真屯,看了似乎很明白很直白脸候,但是還是要提醒一點哦,真理還是在源碼中绑蔫,哈哈哈~

六纪他、LinkedHashMap源碼解析

有了HashMap的源碼分析,LinkedHashMap的源碼應該不難理解晾匠;
在get(key)方法中會根據(jù)訪問的key來,調用makeTail(e)來調整hashMapEntry[] table中的數(shù)據(jù)順序梯刚;

6.1.

LinkedHashMap是一個雙向循環(huán)鏈表凉馆,它的每一個數(shù)據(jù)結點都有兩個指針,分別指向直接前驅和直接后繼亡资,這一個我們可以從它的內部類LinkedEntry中看出澜共,其定義如下:

static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
        LinkedEntry<K, V> nxt;
        LinkedEntry<K, V> prv;

        /** Create the header entry */
        LinkedEntry() {
            super(null, null, 0, null);
            nxt = prv = this;
        }

        /** Create a normal entry */
        LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
                    LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
            super(key, value, hash, next);
            this.nxt = nxt;
            this.prv = prv;
        }
    }

加入一個新的節(jié)點時,執(zhí)行方法锥腻,新插入的節(jié)點放置在尾部:

@Override void addNewEntry(K key, V value, int hash, int index) {
        LinkedEntry<K, V> header = this.header;

        // Remove eldest entry if instructed to do so.
        LinkedEntry<K, V> eldest = header.nxt;
        if (eldest != header && removeEldestEntry(eldest)) {
            remove(eldest.key);
        }

        // Create new entry, link it on to list, and put it into table
        LinkedEntry<K, V> oldTail = header.prv;
        LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
                key, value, hash, table[index], header, oldTail);
        table[index] = oldTail.nxt = header.prv = newTail;
    }

當accessOrder為true時嗦董,訪問一個節(jié)點會執(zhí)行方法makeTail(),將這個節(jié)點移動到尾部瘦黑;

6.2. makeTail(e)方法

    private void makeTail(LinkedEntry<K, V> e) {
        // Unlink e
        e.prv.nxt = e.nxt;
        e.nxt.prv = e.prv;

        // Relink e as tail
        LinkedEntry<K, V> header = this.header;
        LinkedEntry<K, V> oldTail = header.prv;
        e.nxt = header;
        e.prv = oldTail;
        oldTail.nxt = header.prv = e;
        modCount++;
    }

理解makeTail()方法京革,直接看下面兩個過程示意圖:

unlink過程


image

relink過程


image

references:

http://www.cnblogs.com/chenpi/p/5294077.html

https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis/blob/master/article/LruCache%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md

http://www.woaitqs.cc/program/2015/04/14/read-source-code-about-hashmap

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市幸斥,隨后出現(xiàn)的幾起案子匹摇,更是在濱河造成了極大的恐慌,老刑警劉巖甲葬,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件廊勃,死亡現(xiàn)場離奇詭異,居然都是意外死亡经窖,警方通過查閱死者的電腦和手機坡垫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門梭灿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人冰悠,你說我怎么就攤上這事堡妒。” “怎么了屿脐?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵涕蚤,是天一觀的道長。 經常有香客問我的诵,道長万栅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任西疤,我火速辦了婚禮烦粒,結果婚禮上,老公的妹妹穿的比我還像新娘代赁。我一直安慰自己扰她,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布芭碍。 她就那樣靜靜地躺著徒役,像睡著了一般。 火紅的嫁衣襯著肌膚如雪窖壕。 梳的紋絲不亂的頭發(fā)上忧勿,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音瞻讽,去河邊找鬼鸳吸。 笑死,一個胖子當著我的面吹牛速勇,可吹牛的內容都是我干的晌砾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼烦磁,長吁一口氣:“原來是場噩夢啊……” “哼养匈!你這毒婦竟也來了?” 一聲冷哼從身側響起都伪,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤乖寒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后院溺,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體楣嘁,經...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了逐虚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片聋溜。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖叭爱,靈堂內的尸體忽然破棺而出撮躁,到底是詐尸還是另有隱情,我是刑警寧澤买雾,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布把曼,位于F島的核電站,受9級特大地震影響漓穿,放射性物質發(fā)生泄漏嗤军。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一晃危、第九天 我趴在偏房一處隱蔽的房頂上張望叙赚。 院中可真熱鬧,春花似錦僚饭、人聲如沸震叮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽苇瓣。三九已至,卻和暖如春偿乖,著一層夾襖步出監(jiān)牢的瞬間钓简,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工汹想, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撤蚊。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓古掏,卻偏偏與公主長得像,于是被迫代替她去往敵國和親侦啸。 傳聞我的和親對象是個殘疾皇子槽唾,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內容