從零實現(xiàn)ImageLoader(五)—— 內(nèi)存緩存LruCache

目錄

從零實現(xiàn)ImageLoader(一)—— 架構
從零實現(xiàn)ImageLoader(二)—— 基本實現(xiàn)
從零實現(xiàn)ImageLoader(三)—— 線程池詳解
從零實現(xiàn)ImageLoader(四)—— Handler的內(nèi)心獨白
從零實現(xiàn)ImageLoader(五)—— 內(nèi)存緩存LruCache
從零實現(xiàn)ImageLoader(六)—— 磁盤緩存DiskLruCache

LRU算法

在同步加載與異步加載的實現(xiàn)之后固蚤,終于又迎來了圖片加載框架的另一個重要特性——內(nèi)存緩存夕玩。至于加入內(nèi)存緩存原因,之前也已經(jīng)提到了尸昧,主要有以下兩點:

  1. 圖片的加載是需要時間的烹俗,不論是從網(wǎng)絡萍程,還是從磁盤茫负,都會造成應用不同程度的卡頓忍法,而直接從內(nèi)存中加載就可以避免這種情況。
  2. 內(nèi)存的大小并不是無限的缔赠,如果一味的將圖片放入內(nèi)存而不加管理衍锚,很快就會觸發(fā)OOM,所以一個好的內(nèi)存管理也是必不可少的嗤堰。

而Android就給我們提供了這樣一個工具戴质,這就是LruCache

LRU踢匣,全稱Least Recently Used告匠,近期最少使用算法,主要是基于如果數(shù)據(jù)很久沒有被訪問過离唬,那么它將來被訪問的幾率也很小的思想后专。它會優(yōu)先淘汰掉那些很久都沒有被訪問的數(shù)據(jù)输莺,而這也正是內(nèi)存緩存想要做到的事戚哎。

加入內(nèi)存緩存

我們創(chuàng)建一個新類MemoryCache用來封裝LruCache

public class MemoryCache {
    private LruCache<String, Bitmap> mLruCache;

    public MemoryCache(int maxSize) {
        mLruCache = new LruCache<String, Bitmap>(maxSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };
    }

    public Bitmap get(String key) {
        return mLruCache.get(key);
    }

    public void put(String key, Bitmap bitmap) {
        mLruCache.put(key, bitmap);
    }
}

LruCache的使用非常簡單,唯一需要注意的是需要重寫sizeOf()方法嫂用。sizeOf()是用來計算每條數(shù)據(jù)的大小的型凳,默認會返回1,也就是每張圖片都會被當做1Byte對待嘱函,如果不重寫甘畅,恐怕內(nèi)存都撐爆了也沒有一個圖片被淘汰。

現(xiàn)在我們就可以在Dispatcher.get()方法中加入內(nèi)存緩存代碼了:

public Bitmap get() throws IOException {
    //從內(nèi)存獲取
    Bitmap image = mMemoryCache.get(mKey);
    if(image == null) {
        image = NetworkUtil.getBitmap(mUrl);
        synchronized(mMemoryCache) {
            if(mMemoryCache.get(mKey) != null) {
                mMemoryCache.put(mKey, image);
            }
        }
    }
    return image;
}

get()方法會首先嘗試從內(nèi)存中獲取圖片,如果沒有再從網(wǎng)絡上進行下載疏唾,并加入內(nèi)存緩存蓄氧。這里之所以要加鎖重新判斷,主要是為了避免有其他線程在我們下載圖片時將同樣的數(shù)據(jù)已經(jīng)加入緩存槐脏,導致緩存數(shù)據(jù)的重復喉童。

這里的key我們選擇使用圖片URL的MD5值:

private String getKeyByUrl(String url) {
    MessageDigest digest;
    try {
        digest = MessageDigest.getInstance("MD5");
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
    byte[] md5 = digest.digest(url.getBytes());
    StringBuilder result = new StringBuilder();
    for (byte aMd5 : md5) {
        if ((0xFF & aMd5) < 0x10) {
            result.append('0').append(Integer.toHexString(0xFF & aMd5));
        } else {
            result.append(Integer.toHexString(0xFF & aMd5));
        }
    }
    return result.toString();
}

還有一個問題就是圖片緩存的大小,一般來說顿天,為了平衡圖片緩存的效率與內(nèi)存的占用泄朴,我們都會選擇使用1/8內(nèi)存大小:

private int calculateMemoryCacheSize(Context context) {
    ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    int memorySize = activityManager.getMemoryClass() * 1024 * 1024;
    return memorySize / 8;
}

這樣我們就實現(xiàn)了一個完整的內(nèi)存緩存方案露氮。

LruCache原理

當然,我們學習LruCache钟沛,不僅要知其然畔规,還要知其所以然,下面我們就一起來探索LruCache的實現(xiàn)原理恨统。

LruCache內(nèi)部維護了一個LinkedHashMap類叁扫,所有的數(shù)據(jù)都是在這個里面存儲的:

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

get()put()的實現(xiàn)也很簡單:

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

    V mapValue;
    synchronized (this) {
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }

    ...
}

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

    ...

    trimToSize(maxSize);
    return previous;
}

為了邏輯的清晰我去掉了一些回調(diào)代碼,不過剩下的好像除了一些狀態(tài)記錄變量的更新就是對map的put和get了畜埋,難道LRU算法的實現(xiàn)都是在最后一句的trimToSize()方法里莫绣?

public void trimToSize(int maxSize) {
    while (true) {
        K key;
        V value;
        synchronized (this) {
            ...

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

        ...
    }
}

依舊是對LinkedHashMap的操作,連獲取最久的數(shù)據(jù)都是使用LinkedHashMap.eldest()方法悠鞍,原來你是這樣的LruCache对室!看來LRU算法的核心實現(xiàn)都在LinkedHashMap里面了。

LinkedHashMap概覽

LinkedHashMap繼承自HashMap咖祭,所以內(nèi)部數(shù)據(jù)的存儲方式依然是數(shù)組加鏈表掩宜,有所不同的是,LinkedHashMap還單獨維護了一個雙向循環(huán)鏈表么翰,也正是這個鏈表賦予了它排序的功能牺汤。所以LinkedHashMap里的數(shù)據(jù)結構看起來就像這樣:

LinkedHashMap結構

上面的虛線箭頭就代表著LinkedHashMap內(nèi)部維護的雙向循環(huán)鏈表,需要注意的是這里有一個鏈表頭Header浩嫌,而Header是一個空節(jié)點檐迟,且沒有存儲在Hash表內(nèi),只是用來維持鏈表的正常運作的码耐。

而借助這個雙向鏈表追迟,LinkedHashMap內(nèi)部實現(xiàn)了兩種排序方法,由accessOrder狀態(tài)控制伐坏,當accessOrderfalse時會按照數(shù)據(jù)插入的順序排序怔匣,為true時則會按照數(shù)據(jù)訪問的順序排序,而這也就是實現(xiàn)LRU算法的關鍵。

鏈表節(jié)點

要想弄清楚雙向鏈表是怎么實現(xiàn)LRU算法的每瞒,我們首先得搞清楚鏈表節(jié)點是怎么實現(xiàn)的:

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

    /**
     * Removes this entry from the linked list.
     */
    private void remove() {
        before.after = after;
        after.before = before;
    }

    /**
     * Inserts this entry before the specified existing entry in the list.
     */
    private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }

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

    ...
}

和一般的雙向鏈表節(jié)點差不多金闽,LinkedHashMapEntry持有beforeafter兩個節(jié)點。而addBefore()方法確保了每個新插入的數(shù)據(jù)都在老數(shù)據(jù)的前面剿骨。重點則是這里的recordAccess()代芜,該方法會在每次數(shù)據(jù)被訪問的時候調(diào)用,如果accessOrdertrue浓利,也就是按照數(shù)據(jù)訪問順序排序挤庇,該數(shù)據(jù)將會被移動到鏈表的最開始。下面我們就來看看LinkedHashMap到底是不是這樣實現(xiàn)的贷掖。

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

get()方法的邏輯非常簡單嫡秕,調(diào)用父類的getEntry()方法獲取數(shù)據(jù)后,再調(diào)用recordAccess()方法將元素移動到鏈表開頭苹威,實現(xiàn)了越久沒有訪問的數(shù)據(jù)越靠后昆咽。

奇怪的put方法

put()方法就比較麻煩了,我在LinkedHashMap里找了一圈也沒有找到put()方法的實現(xiàn)牙甫,難道LinkedHashMap使用了父類HashMapput()方法掷酗?可這又怎么做到維護雙向鏈表的更新呢?原來HashMap.put()方法在完成數(shù)據(jù)的插入之后窟哺,最終會回調(diào)一個createEntry()方法泻轰,而LinkedHashMap正是依靠重寫這個方法實現(xiàn)的鏈表更新:

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

就在這個方法里LinkedHashMap用自定義的LinkedHashMapEntry節(jié)點替換掉了HashMap創(chuàng)建的HashMapEntry,并且將節(jié)點加入到雙向鏈表的開頭且轨。

eldest方法

public Map.Entry<K, V> eldest() {
    Entry<K, V> eldest = header.after;
    return eldest != header ? eldest : null;
}

由于是雙向鏈表浮声,所以header另一邊肯定就是最后一個數(shù)據(jù)了。

至此殖告,整個LRU算法的實現(xiàn)也就呈現(xiàn)在大家眼前了阿蝶。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市黄绩,隨后出現(xiàn)的幾起案子羡洁,更是在濱河造成了極大的恐慌,老刑警劉巖爽丹,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件筑煮,死亡現(xiàn)場離奇詭異,居然都是意外死亡粤蝎,警方通過查閱死者的電腦和手機真仲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來初澎,“玉大人秸应,你說我怎么就攤上這事虑凛。” “怎么了软啼?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵桑谍,是天一觀的道長。 經(jīng)常有香客問我祸挪,道長锣披,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任贿条,我火速辦了婚禮雹仿,結果婚禮上,老公的妹妹穿的比我還像新娘整以。我一直安慰自己胧辽,他們只是感情好,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布公黑。 她就那樣靜靜地躺著票顾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪帆调。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天豆同,我揣著相機與錄音番刊,去河邊找鬼。 笑死影锈,一個胖子當著我的面吹牛芹务,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鸭廷,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼枣抱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了辆床?” 一聲冷哼從身側響起佳晶,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎讼载,沒想到半個月后轿秧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡咨堤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年菇篡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片一喘。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡驱还,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情议蟆,我是刑警寧澤闷沥,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站咪鲜,受9級特大地震影響狐赡,放射性物質發(fā)生泄漏。R本人自食惡果不足惜疟丙,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一颖侄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦节榜、人聲如沸摊崭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至苔咪,卻和暖如春锰悼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背团赏。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工箕般, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人舔清。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓丝里,卻偏偏與公主長得像,于是被迫代替她去往敵國和親体谒。 傳聞我的和親對象是個殘疾皇子杯聚,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

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