LruCache緩存類源碼解析

LruCache源碼解析

LruCache是Android中的一個緩存工具類送巡,它采用了一種最近最少使用算法芦圾,可以將一些對象進行內(nèi)存緩存,當緩存滿后婴洼,會優(yōu)先刪除近期最少使用的對象兜看。LruCache在實際開發(fā)中是使用率非常高的一個工具類锥咸,許多著名的圖片加載,網(wǎng)絡請求等框架內(nèi)部都是使用的LruCache對象進行數(shù)據(jù)緩存细移,因此我們有必要了解LruCache內(nèi)部的工作原理搏予。

基本使用

LruCache本身是一個泛型類,使用起來也非常簡單弧轧,如果我們想要使用LruCache對象雪侥,首先要實例化一個LruCache對象,并重寫它的sizeOf方法精绎,我們以緩存Bitmap對象為例:

    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    int maxSize = maxMemory / 8;
    LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(maxSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getRowBytes() * value.getHeight() / 1024;
        }
    };

LruCache的構造方法需要傳入一個maxSize參數(shù)速缨,這個參數(shù)代表可以緩存的最大值。而sizeOf方法用來計算要緩存的對象的大小代乃。

如果我們想要緩存一個對象旬牲,只需要調(diào)用LruCache對象的put(K key, V value)方法即可,而當我們想要拿取一個緩存時,則需要調(diào)用get(K key)方法:

    lruCache.put(key, bitmap);
        
    Bitmap cache = lruCache.get(key);

LinkedHashMap對象

我們通過分析LruCache的源碼來分析LruCache的工作原理原茅。首先看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);
    }

我們可以看到吭历,在構造方法中首先將maxSize參數(shù)保存在了一個成員變量中,然后初始化了一個LinkedhashMap對象擂橘。這個LinkedHashMap其實就是LruCache的核心部分晌区,使用LruCache緩存的對象其實是存儲在這個LinkedHashMap中的。

LinkedHashMap是HashMap的一個子類贝室,與HashMap不同的是契讲,LinkedHashMap能夠記住每條記錄的插入順序仿吞。LinkedHashMap類有許多重載的構造方法滑频,而在LruCache中使用的是LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)這個構造方法,其中最關鍵的就是accessOrder這個參數(shù)唤冈,它代表著LinkedhashMap中每條記錄的排序規(guī)則峡迷。當accessOrder為false時,LinkedHashMap是按照插入順序來對每條記錄進行排序的你虹,而當accessOrder為true時绘搞,LinkedHashMap則會采用每條記錄的訪問順序來進行排序。

舉個例子:

    Map<Integer, String> hashMap = new HashMap<>();
    hashMap.put(3, "第一條");
    hashMap.put(2, "第二條");
    hashMap.put(1, "第三條");
    System.out.println("HashMap:\n" + hashMap);

    //采用這個無參的構造方法創(chuàng)建LinkedHashMap時傅物,accessOrder默認為false
    Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
    linkedHashMap.put(3, "第一條");
    linkedHashMap.put(2, "第二條");
    linkedHashMap.put(1, "第三條");
    System.out.println("\nLinkedHashMap:\n" + linkedHashMap);

    Map<Integer, String> linkedHashMap2 = new LinkedHashMap<>(0, 0.75f, true);
    linkedHashMap2.put(3, "第一條");
    linkedHashMap2.put(2, "第二條");
    linkedHashMap2.put(1, "第三條");
    linkedHashMap2.get(2);//在這里訪問了一下"第二條"
    System.out.println("\nLinkedHashMap2:\n" + linkedHashMap2);

輸出結果:

HashMap:
{1=第三條, 2=第二條, 3=第一條}

LinkedHashMap:
{3=第一條, 2=第二條, 1=第三條}

LinkedHashMap2:
{3=第一條, 1=第三條, 2=第二條}

可以看到默認情況下LinkedHashMap是按照順序來存儲每條記錄的夯辖,先插入的在前,后插入的在后董饰,而當accessOrder為true時蒿褂,最近訪問的記錄會被排到后面。LruCache就是根據(jù)LinkedHashMap這個特性來判斷哪些緩存數(shù)據(jù)需要被優(yōu)先清除的卒暂。

插入緩存

LruCache使用put方法來插入一個緩存數(shù)據(jù)啄栓,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);//更新已經(jīng)使用的緩存的大小
            previous = map.put(key, value); //將數(shù)據(jù)插入到LinkedHashMap中
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        return previous;
    }

put方法的源碼非常好理解,首先盤點key和value是否為null也祠,如果為null就直接拋出異常了昙楚。然后使用了synchronized關鍵字來保證線程安全。size變量代表當前已經(jīng)使用的緩存的大小诈嘿,使用sizeOf方法來計算本次提交的緩存數(shù)據(jù)的大小堪旧,并與size相加來更新已經(jīng)使用的緩存大小。之后將本次提交的數(shù)據(jù)插入到LinkedHashMap里奖亚。

在調(diào)用LinkedHashMap的put方法時崎场,如果LinkedHashMap中已經(jīng)存在以這個"Key"為鍵的數(shù)據(jù),則會用新插入的數(shù)據(jù)覆蓋舊的數(shù)據(jù)遂蛀,然后將舊的數(shù)據(jù)返回谭跨,返回的舊數(shù)據(jù)被保存在了previous這個變量內(nèi),如果LinkedHashMap中不存在以這個"Key"為鍵的數(shù)據(jù)則返回null。因此螃宙,當previous不為null時蛮瞄,說明有舊數(shù)據(jù)被覆蓋了,我們要減去這個舊數(shù)據(jù)所占用的空間大小谆扎。

entryRemoved方法會在某個緩存數(shù)據(jù)被移除時被調(diào)用挂捅,默認情況下該方法是個空方法。我們可以根據(jù)需求來重寫這個方法堂湖,在緩存對象要被清除時做一些處理闲先。

最后調(diào)用了trimToSize方法來計算當前緩存數(shù)據(jù)的大小是否超過了緩存允許的最大值的限制,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停止循環(huán)
                    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);
        }
    }

當已用緩存大小超過最大緩存時伺糠,通過map.eldest()來獲取訪問時間最早的那個元素,然后將它從LinkedHashMap中刪除斥季,并重新計算已用緩存的大小训桶,如果已用緩存的大小仍然大于最大緩存大小,則進行下一次循環(huán)繼續(xù)進行刪除酣倾,否則打斷循環(huán)舵揭。

獲取緩存

LruCache對象通過get方法來獲取一個緩存。get方法的源碼如下:

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

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);//從LinkedHashMap中拿取緩存對象
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

        V createdValue = create(key);//通過create方法試圖創(chuàng)建一個對象
        if (createdValue == null) {
            return null;
        }

        //以下代碼與put方法類似
        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

put方法的代碼略長躁锡,但理解起來其實非常簡單午绳。依舊是宣判斷key是否為null。之后通過map.get(key)來從LinkedHashMap中獲取緩存的數(shù)據(jù)映之。如果獲取的數(shù)據(jù)不為null拦焚,則直接將其返回,并且LinkedHashMap會自動將這個緩存對象排列到最后面惕医。

如果map.get(key)獲取的值為空耕漱,則會試圖調(diào)用create方法來創(chuàng)建一個對象,create默認情況下直接返回null抬伺。我們可以根據(jù)需求重寫create方法來實現(xiàn)自己想要的結果螟够。如果create成功的創(chuàng)建了一個對象,LruCache則會將這個對象添加到LinkedHashMap中峡钓,并返回該對象妓笙。

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

在理解了put和get方法后能岩,remove的源碼讀起來就毫無壓力了寞宫。通過map.remove(key)來直接從LinkedHashMap中移除該對象,并更新size的值拉鹃,然后調(diào)用entryRemoved方法進行處理辈赋。代碼很簡單鲫忍,就不再多說了。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钥屈,一起剝皮案震驚了整個濱河市悟民,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌篷就,老刑警劉巖射亏,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異竭业,居然都是意外死亡智润,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門未辆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窟绷,“玉大人,你說我怎么就攤上這事鼎姐〖佤铮” “怎么了更振?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵炕桨,是天一觀的道長。 經(jīng)常有香客問我肯腕,道長献宫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任实撒,我火速辦了婚禮姊途,結果婚禮上,老公的妹妹穿的比我還像新娘知态。我一直安慰自己捷兰,他們只是感情好,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布负敏。 她就那樣靜靜地躺著贡茅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪其做。 梳的紋絲不亂的頭發(fā)上顶考,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天,我揣著相機與錄音妖泄,去河邊找鬼驹沿。 笑死,一個胖子當著我的面吹牛蹈胡,可吹牛的內(nèi)容都是我干的渊季。 我是一名探鬼主播朋蔫,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼却汉!你這毒婦竟也來了斑举?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤病涨,失蹤者是張志新(化名)和其女友劉穎富玷,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體既穆,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡赎懦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了幻工。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片励两。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖囊颅,靈堂內(nèi)的尸體忽然破棺而出当悔,到底是詐尸還是另有隱情,我是刑警寧澤踢代,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布盲憎,位于F島的核電站,受9級特大地震影響胳挎,放射性物質(zhì)發(fā)生泄漏饼疙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一慕爬、第九天 我趴在偏房一處隱蔽的房頂上張望窑眯。 院中可真熱鬧,春花似錦医窿、人聲如沸磅甩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卷要。三九已至,卻和暖如春隔显,著一層夾襖步出監(jiān)牢的瞬間却妨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工括眠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留彪标,地道東北人。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓掷豺,卻偏偏與公主長得像捞烟,于是被迫代替她去往敵國和親薄声。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355

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