LRU演算法還一知半解?

倩倩_糖葫蘆發表於2017-12-21

某年某月某日,糖葫蘆同學在掘金app上看了幾篇文章,偶然看到了一篇熟悉的詞LRU演算法,腦海裡就想這不是經常說的嘛,就那麼回事,當天晚上睡覺,LRU演算法是啥來著,好像是什麼最近最少使用的,白天在地鐵上看的文章也不少,但是到晚上想想好像啥也沒記住,就記得LRU演算法,我發現人大多數是這樣的啊,對於自己熟悉的部分呢還能記著點,不熟悉或者不會的可能真的是看過就忘啊~既然這樣還不如先把熟悉的弄明白。

第二天來到公司,我覺著還是有必要看一下這個LRU的原始碼,到底是怎麼回事,嗯,糖葫蘆同學刷刷得看,下面我們將進入正題,請戴眼鏡的同學把眼鏡擦一擦,哈哈哈

First

先看原始碼,再用具體的demo加以驗證,我們先看一下這個LruCache這個類的大致結構和方法,如下圖所示:

image.png

這又是 get(K),put(K,V), remove(K) 的方法的 給人的感覺就像是一個Map的集合嘛,又有Key ,又有value 的,再看下具體的程式碼:

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;
    private int maxSize;

    private int putCount;
    private int createCount;
    private int evictionCount;
    private int hitCount;
    private int missCount;

    /**
     * @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);
    }
複製程式碼

看到開頭,我們就明白了,哦原來這個LruCache類中維護一個LinkedHashMap的一個集合,快取我們這個物件,而且構造方法裡需要我們傳入一個maxSize的一個值,根據上面的註釋我們就明白了這個就是我們LruCache快取物件的最大數目。

有什麼用呢?

根據慣性思維,我們可以認為,在put新的快取物件的時候,根據我們設定的最大值remove集合裡的某些快取物件,進而新增新的快取物件。

Second

根據我們的分析,我們有必要去看一下這個put方法的原始碼:

    /**
     * Caches {@code value} for {@code key}. The value is moved to the head of
     * the queue.
     *
     * @return the previous value mapped by {@code key}.
     */
    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;
    }
複製程式碼

程式碼量也不是特別多,我們看下這個,在這個synchronized同步程式碼塊裡,我們看到這個 size,是對put進來快取物件個數的累加,然後呼叫集合的map.put方法,返回一個物件 previous ,就是判斷這個集合中是否新增了這個快取物件,如果不為null,就對size減回去。

最後又呼叫一個 trimToSize(maxSize)方法,上面都是對新增一些邏輯的處理,那麼不可能無限制新增啊,肯定有移除操作,那麼我們推測這個邏輯可能在這個trimToSize(maxSize) 裡處理。

原始碼如下:

/**
     * Remove the eldest entries until the total of remaining entries is at or
     * below the requested size.
     *
     * @param maxSize the maximum size of the cache before returning. May be -1
     *            to evict even 0-sized elements.
     */
    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!");
                }

                //只要當前size<= maxSize 就結束迴圈
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
                // 獲取這個物件,然後從map中移除掉,保證size<=maxSize
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }
複製程式碼

註釋:Remove the eldest entries until the total of remaining entries is at or below the requested size 大概意思是說:清除時間最久的物件直到剩餘快取物件的大小小於設定的大小。沒錯是我們想找的。

這裡說明一下:maxSize就是我們在構造方法裡傳入的,自己設定的

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);
    }
複製程式碼

這樣LruCache的核心方法 trimToSize方法我們就說完了,接下來我將通過例項再次驗證下:

設定場景

假設我們設定maxSize 為2,佈局裡顯示3個imageView,分別代表3張我們要顯示的圖片,我們新增3張圖片,看看會不會顯示3張?

xml佈局顯示如下(程式碼就不貼了,很簡單):

image.png

activity程式碼如下:

public final int MAX_SIZE = 2;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_lru);

        ImageView iv1 = (ImageView) findViewById(R.id.iv1);
        ImageView iv2 = (ImageView) findViewById(R.id.iv2);
        ImageView iv3 = (ImageView) findViewById(R.id.iv3);

        Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(),R.drawable.bg);
        Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(),R.drawable.header_img);
        Bitmap bitmap3 = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);

        LruCache<String,Bitmap> lruCache = new LruCache<>(MAX_SIZE);
        lruCache.put("1",bitmap1);
        lruCache.put("2",bitmap2);
        lruCache.put("3",bitmap3);

        Bitmap bitmap = lruCache.get("1");
        iv1.setImageBitmap(bitmap);

        Bitmap b2 = lruCache.get("2");
        iv2.setImageBitmap(b2);

        Bitmap b3 = lruCache.get("3");
        iv3.setImageBitmap(b3);
    }
複製程式碼

圖:

bg.png
header_img.png
ic_launcher.png

我們可以先嚐試分析一下:因為我們設定的MaxSize 是2 ,那麼在put第三個Bitmap的時候,在trimToSize方法中,發現這個size是3 ,maxSize 是2,會繼續向下執行,不會break,結合下面程式碼看下

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!");
                }
                //第一次迴圈:此時 size 是3,maxSize 是 2
                //第二次迴圈,此時 size 是 2 ,maxSize 是 2 ,滿足條件,break,結束迴圈
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
              //獲取最先新增的第一個元素
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
              //移除掉第一個快取物件
                map.remove(key);
              // size = 2,減去移除的元素
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

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

複製程式碼

這個 safeSizeOf 是呼叫sizeOf方法。

那麼也就是說,我們在put第三個bitmap的時候,LruCache 會自動幫我們移除掉第一個快取物件,因為第一個最先新增進去,時間也最長,當然後新增的bitmap就是新的,最近的,那麼我們推斷這個iv1是顯示不出圖片的,因為被移除掉了,其它剩餘兩個可以顯示,分析就到這裡,看下執行結果是不是跟我們分析的一樣:

result.png

哇!真的跟我們想的一樣耶,證明我們想的是對的。這裡我們思考一下就是為什麼LruCache使用了這個LinkedHashMap,為什麼LinkedHashMap的創造方法跟我們平時建立的不太一樣,原始碼是這樣的:

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);
    }
複製程式碼

這裡說一下評論裡 藏地情人評論是:new LinkedHashMap<K, V>(0, 0.75f, true)這句程式碼表示,初始容量為零,0.75是載入因子,表示容量達到最大容量的75%的時候會把記憶體增加一半。最後這個引數至關重要。表示訪問元素的排序方式,true表示按照訪問順序排序,false表示按照插入的順序排序。這個設定為true的時候,如果對一個元素進行了操作(put、get),就會把那個元素放到集合的最後。

確實也是這樣的,我們看下LinkedHashMap的原始碼:

/**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
複製程式碼

裡面這個assessOrder 註釋裡也說的很明白:the ordering mode - <tt>true</tt> for * access-order, <tt>false</tt> for insertion-order -> true 呢就表示會排序,false 就代表按照插入的順序。預設不傳就是 false ,而且我們每次 get(K) put(K,V) 的時候 會根據這個變數調整元素在集合裡的位置。而這麼做的目的也只有一個:保留最近使用的快取物件,舉個例子說明一下:

我們向這個集合裡新增了三種元素

    LruCache<String, Bitmap> lruCache = new LruCache<>(MAX_SIZE);(MAX_SIZE=2)
    lruCache.put("1", bitmap1);
    lruCache.put("2", bitmap2);
    lruCache.put("3", bitmap3);
複製程式碼

此時它們在集合裡的順序是這樣的:

order.png

那比如說我們在put 3 元素之前,使用了1元素,就是呼叫了get("1")方法,我們知道LinkedHashMap就會改變連結串列裡元素的儲存順序,程式碼是這樣的:

        lruCache.put("1", bitmap1);
        lruCache.put("2", bitmap2);
        lruCache.get("1");
        lruCache.put("3", bitmap3);
複製程式碼
那麼此時對應連結串列裡的順序就是:
複製程式碼

image.png

當我們再呼叫顯示的時候,迴圈遍歷就會優先把第一個位置的key = "2" 的快取物件移除掉,保證了最近使用的原則,當然了因為把這個max_size = 2所以在我們執行lruCache.put("3", bitmap3); 時,集合最終會變成這樣:

result.png

集合裡只剩下 1 ,3對應的快取物件。

至此,LruCache就說完了,如果看完的你有不明白的地方可以留言,一起討論下~

相關文章