LRU算法還只知其一;不知其二?

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

次日來到公司,我覺着仍是有必要看一下這個LRU的源碼,究竟是怎麼回事,嗯,糖葫蘆同窗刷刷得看,下面咱們將進入正題,請戴眼鏡的同窗把眼鏡擦一擦,哈哈哈算法

First

先看源碼,再用具體的demo加以驗證,咱們先看一下這個LruCache這個類的大體結構和方法,以下圖所示: 緩存

image.png

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

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緩存對象的最大數目。ide

有什麼用呢?

根據慣性思惟,咱們能夠認爲,在put新的緩存對象的時候,根據咱們設定的最大值remove集合裏的某些緩存對象,進而添加新的緩存對象。佈局

Second

根據咱們的分析,咱們有必要去看一下這個put方法的源碼:this

/** * 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減回去。spa

最後又調用一個 trimToSize(maxSize)方法,上面都是對添加一些邏輯的處理,那麼不可能無限制添加啊,確定有移除操做,那麼咱們推測這個邏輯可能在這個trimToSize(maxSize) 裏處理。3d

源碼以下:code

/** * 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就說完了,若是看完的你有不明白的地方能夠留言,一塊兒討論下~

相關文章
相關標籤/搜索