Picasso解析(2)-LruCache緩存分析

0.前言

上一次 Picasso解析(1)-一張圖片是如何加載出來的中,我已經(jīng)將整個Picasso如何解析一張網(wǎng)絡(luò)圖片的過程梳理了一遍。如果僅僅只是這樣梳理一遍,那就只是為了看代碼而看代碼了,真正的我們還是應(yīng)該學(xué)習(xí)到這些高質(zhì)量代碼中優(yōu)美的設(shè)計與精湛的技巧。從這一篇開始,我就會逐一將我在當(dāng)中學(xué)習(xí)到的地方分享出來。首先就從LruCache開始分析。

1.最近最少使用算法

LRU是Least Recently Used 最近最少使用算法,相信大學(xué)時候?qū)W過操作系統(tǒng)這門課的同學(xué)一定不會陌生。作為一種在內(nèi)存當(dāng)中存取數(shù)據(jù)的策略,LRU算法的本質(zhì)是希望提高內(nèi)存數(shù)據(jù)使用的命中率。核心思想就是:如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也會更高。
具體一點舉個例子,比如說我們有一個大小為3的內(nèi)存塊,現(xiàn)在我們有4,1,4,2,1,5,7,2八組數(shù)據(jù)要進(jìn)入我們的內(nèi)存塊,那么我們來看一下內(nèi)存塊中的變化情況,在這里我們規(guī)定右邊的數(shù)據(jù)是最近使用的數(shù)據(jù)
(1)4:4
(2)1:4,1
(3)4:1,4
(4)2:1,4,2
(5)1:4,2,1
(6)5:2,1,5
(7)7:1,5,7
(8)2:5,7,2
在這里我們可以很清晰地看出來,在LRU算法中,最近使用過的數(shù)據(jù)總是在隊列的前面,總是淘汰處于隊列中最末尾的數(shù)據(jù)。

2.自己動手實現(xiàn)LRUCache

在這里我用一個很簡單很暴力的寫法來演示一下LRU算法的原理:

public class LRUDemo {
    public static void main(String[] args) {
        LRU lru = new LRU(3);
        int[] arr = {4, 1, 4, 2, 1, 5, 7, 2};
        for (int i = 0; i < arr.length; i++) {
            lru.set(arr[i]);
            lru.traversal();
            System.out.println();
        }
    }
}

class LRU {
    private Queue<Integer> mLRUList;
    private int mMaxSize;
    private int mCurrentSize;

    public LRU(int maxSize) {
        mLRUList = new LinkedList<>();
        mMaxSize = maxSize;
        mCurrentSize = 0;
    }

    public void set(Integer data) {
        isExist(data);

        if (mCurrentSize >= mMaxSize) {
            mLRUList.poll();
        }

        mCurrentSize++;
        mLRUList.add(data);
    }

    public void isExist(int data) {
        if(mLRUList.remove(data)) {
            mCurrentSize--;
        }
    }

    public void traversal() {
        for (Integer i : mLRUList) {
            System.out.print(i + " ");
        }
    }
}

運行結(jié)果:


運行結(jié)果

在這里,我采用了LinkedList來模擬一個大小為3的隊列,用了一種很暴力的方法,只要發(fā)現(xiàn)隊列中存在重復(fù)的元素,就直接刪除掉,然后將新的數(shù)據(jù)添加到隊列的頭部,這樣就可以保證隊列從頭部到尾部依次是最近使用過的數(shù)據(jù)了。這里只是為了演示LRU的原理,這種寫法當(dāng)然是不提倡的。

3.Picasso中LRUCache的實現(xiàn)原理

前面主要針對LRU算法的一些原理進(jìn)行了解釋,接下來才是本文的核心部分,關(guān)于Picasso源碼中的LRUCache實現(xiàn)原理的分析。

其實在android.util包中也有為我們提供LRUCache算法的實現(xiàn),但是Picasso的作者還是自己實現(xiàn)了一個。我對比著看過了這兩種LRUCache代碼的實現(xiàn),核心的實現(xiàn)思路都是一樣。所以這里的分析以Picasso中的LRUCache為準(zhǔn)。

(1).構(gòu)造方法

  public LruCache(@NonNull Context context) {
    this(Utils.calculateMemoryCacheSize(context));
  }
  
  public LruCache(int maxSize) {
    if (maxSize <= 0) {
      throw new IllegalArgumentException("Max size must be positive.");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
  }

第一種構(gòu)造方法是通過Utils方法里提供的計算內(nèi)存方法來指定大小,其中在Picasso中需要的內(nèi)存是當(dāng)前應(yīng)用內(nèi)存的七分之一,也就是15%左右,具體為什么是七分之一我并不知道原因,個人認(rèn)為應(yīng)該是JakeWharton大神經(jīng)過反復(fù)實踐得出的最合理分配,所以我們也就默認(rèn)這么大了。在第二個構(gòu)造方法中,我們可以看到該方法初始化了一個LinkedMap成員變量用來存儲圖片。而這個LinkedMap的構(gòu)造方法則與我們平常所使用的不太一樣。我特意翻看了一下文檔:


LinkedHashMap

第一個參數(shù)指定的是初始化容器大小。第二個參數(shù)是加載因子,這里傳入的是0.75f。而第三個參數(shù)則是指定排序方法,如果為true,則是按照最常訪問來進(jìn)行排序,如果為false,則是按照插入時間來進(jìn)行排序,在這里指定為true也很符合LRU算法的理念。所以我們重點來關(guān)注一下第二個參數(shù),順著構(gòu)造方法進(jìn)去閱讀一下源碼吧:

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


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

我們看到loadFactor參數(shù)居然除了簡單判斷了一下以外完全沒有用到,google的程序員們非常善意的給我們留下了note,大概意思就是說我們總是使用默認(rèn)值0.75。但還是令我感到一臉懵逼,于是我就到JAVA的官方文檔里去看,發(fā)現(xiàn)了如下一段解釋:

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

大概的意思是說,0.75這個值是權(quán)衡時間與空間利弊以后的一個最佳值,如果高于這個值,會節(jié)省一些空間,但會影響時間效率。我們再來看看Android源碼中HashMap的一段注解:

    // Android-Note: We always use a load factor of 0.75 and ignore any explicitly
    // selected values.
    final float loadFactor = DEFAULT_LOAD_FACTOR;

google的大神們直接認(rèn)準(zhǔn)了0.75這個值,就算你傳參傳了個其他值也沒用,所以難怪我們在看源碼的時候沒有看到任何的賦值操作,而只是做了一個簡單的判斷而已。當(dāng)然,至于這個值為什么要這樣設(shè)定,時間與空間上是如何平衡的,不在本文的討論范圍之內(nèi),等以后分析JAVA集合框架的時候在仔細(xì)研究。

(2).set方法

@Override
    public void set(@NonNull String key, @NonNull Bitmap bitmap) {
        if (key == null || bitmap == null) {
            throw new NullPointerException("key == null || bitmap == null");
        }

        int addedSize = Utils.getBitmapBytes(bitmap);
        if (addedSize > maxSize) {
            return;
        }

        synchronized (this) {
            putCount++;
            size += addedSize;
            Bitmap previous = map.put(key, bitmap);
            if (previous != null) {
                size -= Utils.getBitmapBytes(previous);
            }
        }

        trimToSize(maxSize);
    }

我們來看看核心的set方法。首先做了一些簡單的判斷,然后開始計算Bitmap的大小,如果說圖片的大小直接就比我們整個緩存的大小都大的話,那就直接return掉了,如果不是的話,就進(jìn)入一段同步代碼塊(保證多線程加載圖片情況下的線程安全)。在這段同步代碼塊里,首先會將bitmap put進(jìn)LinkedHashmap中,當(dāng)Map中存在同樣key的value時,put方法就會返回給舊的值,同時也會根據(jù)添加和替換的結(jié)果來計算當(dāng)前內(nèi)存中的size。同步代碼塊的代碼執(zhí)行完畢后會做一個trimToSize操作:


private void trimToSize(int maxSize) {
        while (true) {
            String key;
            Bitmap 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<String, Bitmap> toEvict = map.entrySet().iterator()
                        .next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= Utils.getBitmapBytes(value);
                evictionCount++;
            }
        }
    }

一上來我們就發(fā)現(xiàn)這是一個無限循環(huán),該方法主要就是根據(jù)當(dāng)前的size大小來進(jìn)行一個判斷。如果當(dāng)前的size沒有超過maxSize的話,循環(huán)就會中指,如果size大于maxSize的話,就會刪除掉最久沒有使用的圖片,并一直到size最終小于maxSize時終止。那么,在這里為什么我們通map.entrySet().iterator().next()方法就能拿到最久沒有使用的對象呢?這就涉及到之前構(gòu)造方法里傳參的第三個參數(shù)accessOrder,當(dāng)它為true的時候,就是按照訪問的順序來進(jìn)行排序,所以我們才能順利地拿到這個需要刪除的對象,其中具體的原理也是等到以后分析JAVA集合框架的時候再說吧。

4.總結(jié)

至此LRUCache緩存類的原理就分析完畢了,核心的思想便是一個最近最少使用算法。我們可以看到JakeWharton大神巧妙的利用到了LinkedHashmap中的一些高級特性來實現(xiàn)了一個LRU算法,為我們以后設(shè)計諸如此類算法策略時提供了一些新的思路。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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