性能優(yōu)化(2.3)-LruCache源碼解析

主目錄見:Android高級進階知識(這是總目錄索引)
?今天我們來聊聊緩存策略相關的內(nèi)容,LruCache應該說是三級緩存策略會使用到的內(nèi)存緩存策略。今天我們就來扒一扒這里面的原理,同時也溫故溫故我們的數(shù)據(jù)結(jié)構(gòu)方面的知識。

一.目標

我們今天講的這個緩存策略,主要有幾個目的:
1.了解緩存的策略;
2.鞏固數(shù)據(jù)結(jié)構(gòu)相關的知識;
3.自己能實現(xiàn)一個緩存策略。

二.源碼解析

1.緩存策略

要來分析源碼,我們首先要先明白有哪幾種緩存淘汰算法,我們先來復習一下:

1.FIFO(First In First Out):先進先出;
2.LRU(Least Recently Used):最近最少使用;
3.LFU(Least Frequently Used):最不經(jīng)常使用。

這些都是什么呢?我們舉個例子,比如我們的緩存對象順序為:(隊尾)EDDCBABAEA(隊頭),那么如果這時候來了個A,這時候要淘汰一個對象,如果是FIFO,這時候就會淘汰的E;如果是LRU的話,這時候就會淘汰的D,因為D被使用過之后接下來再也沒有被使用過了;如果是LFU的話,那么淘汰的就是C了,因為C就被使用過一次。這些就是我們?nèi)齻€緩存淘汰算法,我們知道我們的緩存是有限的,所以我們必須在新的對象進來的時候選擇一個優(yōu)秀的替換策略來替換緩存中的對象,這樣可以提高緩存的命中率,進而提高我們程序的效率。

2.LinkedHashMap

我們知道,我們的LRU算法可以用很多方法實現(xiàn),最常見的是用鏈表的形式,這里的LinkedHashMap就是雙向鏈表實現(xiàn)的,所以我們的LruCache是用的LinkedHashMap來實現(xiàn),我們首先看下LruCache的成員變量和構(gòu)造函數(shù):

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;

    /** Size of this cache in units. Not necessarily the number of elements. */
    private int size;//緩存內(nèi)容大小
    private int maxSize;//最大的緩存大小

    private int putCount;//put()方法被調(diào)用的次數(shù)
    private int createCount;//create()方法被調(diào)用的次數(shù)
    private int evictionCount;// 被置換出來的元素的個數(shù)
    private int hitCount;//命中緩存中對象的次數(shù)
    private int missCount;//未命中緩存中對象的次數(shù)

    /**
     * @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);//第一個參數(shù)是初始化容量,第二個參數(shù)是加載因子默認是0.75,第三個為訪問順序
    }
......
}

我們先來說說初始化容量和加載因子的關系,我們這里下來看下HashMap中的構(gòu)造函數(shù):

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY) {
            initialCapacity = MAXIMUM_CAPACITY;
        } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
            initialCapacity = DEFAULT_INITIAL_CAPACITY;
        }

        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // Android-Note: We always use the default load factor of 0.75f.

        // This might appear wrong but it's just awkward design. We always call
        // inflateTable() when table == EMPTY_TABLE. That method will take "threshold"
        // to mean "capacity" and then replace it with the real threshold (i.e, multiplied with
        // the load factor).
        threshold = initialCapacity;
        init();
    }

我們看到我們的初始容量為0的話,這里會使用默認的初始容量,然后如果我們進行擴容的時候會用到 float thresholdFloat = capacity * loadFactor即容量*加載因子來進行決定擴展后的容量,默認的加載因子0.75是實驗后的最佳數(shù)據(jù)。接著我們來看看LinkedHashMap是怎么實現(xiàn)的LRU算法的,我們先來LinkedHashMap的變量和構(gòu)造函數(shù):

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{

    private static final long serialVersionUID = 3801124242820219131L;
    private transient LinkedHashMapEntry<K,V> header;

    private final boolean accessOrder;

    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
......
}

我們看到LinkedHashMap里面主要有LinkedHashMapEntry,這個是雙向鏈表的一個節(jié)點有前驅(qū)和后繼,我們可以來看看這個LinkedHashMapEntry節(jié)點:

  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,同時里面有before和after節(jié)點,這是為了擴展成雙向鏈表做的準備。我們來看下添加新的節(jié)點的方法最終會調(diào)用到createEntry方法:

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

看懂這個方法之前,我們必須明確一下hashmap的數(shù)據(jù)結(jié)構(gòu),我們看下下面這個圖:

LinkedHashMap完整的數(shù)據(jù)結(jié)構(gòu)

我們看到前面會有一個table數(shù)組用于存放各個entry鏈表的,然后LinkedHashMap又在此基礎上面增加了當前節(jié)點上面增加before和after的前驅(qū)和后繼節(jié)點的引用信息。為了大家更加清楚地知道這個雙鏈表結(jié)構(gòu),我們把雙鏈表抽取出來如下:


雙向鏈表

所以添加一個新的節(jié)點的時候會調(diào)用addbefore來添加,這個方法做的東西就是在頭部增加新的節(jié)點:


新增新的節(jié)點

具體的代碼在addBefore()方法里面:
 private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

這里的existingEntry就是我們的header。所以我們可以看到新增的節(jié)點被插入到了首節(jié)點前面變成了首節(jié)點。我們剛才看到LruCache構(gòu)造函數(shù)里面LinkedHashMap的初始化的第三個參數(shù)accessOrder被賦值為true是什么意思呢?這個是為了記錄訪問的順序的,如果被訪問過了之后,這里true說明我們要把被訪問過的節(jié)點掉到首節(jié)點去。具體代碼可以看recordAccess()方法:

 void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }

這個方法是在get方法中調(diào)用的,我們這里如果accessOrder為true的話,那么我們會先移除訪問節(jié)點,然后把它添加到首節(jié)點,說明我這個節(jié)點剛訪問過。到這里我們已經(jīng)明白了LinkedHashMap的工作原理了,那么我們接下來就來看看LruCache的源碼了。

3.LruCache源碼

熟悉了LinkedHashMap的數(shù)據(jù)結(jié)構(gòu),我們就很容易知道怎么用這個來實現(xiàn)LRU算法了,我們先來看看LruCache的get()方法的源碼:

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

        V mapValue;
        synchronized (this) {
//現(xiàn)在hashMap中查找有沒有這個key對應的節(jié)點(這個地方只要是get一次就會把命中的節(jié)點往首節(jié)點排)
            mapValue = map.get(key);
            if (mapValue != null) {
//如果命中的話那么命中+1,返回該值
                hitCount++;
                return mapValue;
            }
//如果沒有命中的話那么沒命中+1
            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.
         */
//嘗試去創(chuàng)建一個值,默認是空
        V createdValue = create(key);
        if (createdValue == null) {//如果不為沒有命名的key創(chuàng)建新值,則直接返回
            return null;
        }
// 接下來是如果用戶重寫了create方法后,可能會執(zhí)行到
        synchronized (this) {
            createCount++;//創(chuàng)建的數(shù)量增加
            mapValue = map.put(key, createdValue););// 將剛剛創(chuàng)建的值放入map中,返回的值是在map中與key相對應的舊值(就是在放入new value前的old value) 

            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);//如果不為空,說明不需要我們所創(chuàng)建的值,所以又把返回的值放進去
            } else {
                size += safeSizeOf(key, createdValue);//為空,說明我們更新了這個key的值,需要重新計算大小
            }
        }

        if (mapValue != null) {//上面放入的值有沖突
            entryRemoved(false, key, createdValue, mapValue);// 通知之前創(chuàng)建的值已經(jīng)被移除,而改為mapValue
            return mapValue;
        } else {
            trimToSize(maxSize);//沒有沖突時,因為放入了新創(chuàng)建的值,大小已經(jīng)有變化,所以需要修整大小
            return createdValue;
        }
    }

我們看到LruCahe是可能被多個線程訪問的,所以讀取時候要適當加上鎖機制,當獲取不到key對應的value時候,他會調(diào)用create方法,這個方法默認是返回null的,除非我們重寫了create方法,這個方法并沒有加鎖,所以在創(chuàng)建的過程中有可能其他線程已經(jīng)添加進去了這個值,所以在后面的時候會進行判斷是否已經(jīng)不為空了,如果不為空即刪除放入原來的值,沒有沖突就放入新值調(diào)整大小變化。我們來看下最后調(diào)整大小的代碼trimToSize方法:

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

                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

這個方法我們看到會判斷size<=maxSize不,如果小于則不用調(diào)整,如果大于了那么我們就會取出最老的Entry,進行刪除,然后置換的個數(shù)增加1。然后我們看下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;
    }

我們看到這個方法不難,主要的邏輯就是計算一下放入進去值的大小,然后加起來。同樣地,放進去map中,然后看是不是更新舊的值,如果是則把剛才加上的大小再減去,然后刪除舊的值跟maxSize調(diào)整一下總的大小。到這里我們大概已經(jīng)講完LruCache的源碼了,我們也大概了解了整體的設計,其實我們自己也是可以寫出這樣一套代碼的,主要的還是數(shù)據(jù)結(jié)構(gòu)方面的知識。

總結(jié):其實整體的LruCache的實現(xiàn)并不會非常難,主要就是數(shù)據(jù)結(jié)構(gòu)的知識,我們可以根據(jù)這一套思想,我們也可以實現(xiàn)各種緩存策略,今天講的這個主要是內(nèi)存的緩存策略,到時我們可以來講講DiskLruCache是磁盤的緩存策略,希望能有所收獲哈。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內(nèi)容