LruCache 一、簡介 二、代碼分析 三、總結

Android知識總結

一、簡介

LruCache(Least Recently Used)算法的核心思想就是最近最少使用算法。他在算法的內部維護了一個LinkHashMap的鏈表,LinkedHashMap 是由數組+雙向鏈表的數據結構來實現的,通過put數據的時候判斷是否內存已經滿了,如果滿了,則將最近最少使用的數據給剔除掉,從而達到內存不會爆滿的狀態。

通過上面這張圖,我們可以看到,LruCache算法內部其實是一個隊列的形式在存儲數據,先進來的數據放在隊列的尾部,後進來的數據放在隊列頭部,如果要使用隊列中的數據,那麼使得之後將其又放在隊列的頭部,如果要存儲數據並且發現數據已經滿了,那麼便將隊列尾部的數據給剔除掉,從而達到我們使用緩存的目的。這裏需要注意一點,隊尾存儲的數據就是我們最近最少使用的數據,也就是我們在內存滿的時候需要剔除掉的數據。

二、代碼分析

1、構造方法和參數

public class LruCache<K, V> {
    //定義一個LinkedHashMap,有序的 map
    private final LinkedHashMap<K, V> map;

    private int size;//初始大小
    private int maxSize;//最大容量

    private int putCount;//插入個數
    private int createCount;//創建個數
    private int evictionCount;//回收個數
    private int hitCount;//找到key的個數
    private int missCount;//沒找到key的個數

    //構造函數,傳遞進來一個最大容量值
    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);
    }

    //設置cache的大小
    public void resize(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

        synchronized (this) {
            this.maxSize = maxSize;
        }
        trimToSize(maxSize);
    }
}

2、 get 方法分析

     //如果通過key查找到value存在於cache中就直接返回或者通過create方法創建一個然後返回
     //如果這個值被返回了,那麼它將移動到隊列的頭部
     //如果一個值沒有被緩存同時也不能被創建則返回null
    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key); // 獲取 Value
            //找到對應值,命中+1,直接返回該值
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            //否則未命中+1
            missCount++;
        }

        //如果沒找到key對應的值那麼就嘗試創建一個,也許花費較長的時間
        //並且創建後返回的map也許和之前的不同,如果創建的值和map中有衝突的話
        //那麼我們就釋放掉創建的值,保留map中的值。

        V createdValue = create(key);
        //通過觀察後面的create()方法,可以看到直接return null;
        //那麼我們需要想一想爲什麼源碼中是直接返回null呢?
        //因爲LruCache常常作爲內存緩存而存在,所以當我們查找key找不到對應的value時
        //這個時候我們應該從其他方面,比如文件緩存或者網絡中請求數據
        //而不是我們隨便賦值創建一個值返回,所以這裏返回null是合理的。
        //如果自己真的有需要的話,自己需要重寫create方法,手動創建一個值返回
        if (createdValue == null) {
            return null;
        }

        //走到這兒說明創建了一個不爲null的值
        synchronized (this) {
            createCount++;//創建個數+1
            //把創建的value插入到map對應的key中
            //並且將原來鍵爲key的對象保存到mapValue
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                //如果mapValue不爲空,說明原來key對應的是有值的,則撤銷上一步的put操作。
                map.put(key, mapValue);
            } else {
                //加入新創建的對象之後需要重新計算size大小
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            //每次新加入對象都需要調用trimToSize方法看是否需要回收
            trimToSize(maxSize);
            return createdValue;
        }
    }
  • LinkedHashMap 的 get 方法
public V get(Object key) {
    Node < K, V > e;
    if ((e = getNode(hash(key), key)) == null) 
        return null;
    if (accessOrder)
       afterNodeAccess(e);
    return e.value;
}

get() 方法其實最關鍵就是 afterNodeAccess(),現在重點分析:

// 這個方法的作用就是將剛訪問過的元素放到集合的最後一位
void afterNodeAccess(Node < K, V > e) {
    LinkedHashMap.Entry < K, V > last;
    if (accessOrder && (last = tail) != e) {
        // 將 e 轉換成 LinkedHashMap.Entry
        // b 就是這個節點之前的節點
        // a 就是這個節點之後的節點
        LinkedHashMap.Entry < K, V > p = (LinkedHashMap.Entry < K, V > ) e, b = p.before, a = p.after;
        
        // 將這個節點之後的節點置爲 null
        p.after = null;
        
        // b 爲 null,則代表這個節點是第一個節點,將它後面的節點置爲第一個節點
        if (b == null) head = a;
        
        // 如果不是,則將 a 上前移動一位
        else b.after = a;
        // 如果 a 不爲 null,則將 a 節點的元素變爲 b
        if (a != null) a.before = b;
        else last = b;
        if (last == null) head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

3、put 方法分析

現在以註釋的方式來解釋該方法的原理。

    //將key對應的value緩存起來,放在隊列的頭部
    //返回key對應的之前的舊值
    public final V put(K key, V value) {
        // 如果 key 或者 value 爲 null,則拋出異常
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            // 加入元素的數量,在 putCount() 用到
            putCount++;//插入數量+1
            // 回調用 sizeOf(K key, V value) 方法,這個方法用戶自己實現,默認返回 1
            size += safeSizeOf(key, value);
            //得到key對應的前一個value,如果之前無值,返回null,如果有值,返回前一個值
            previous = map.put(key, value);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            // 該方法默認方法體爲空
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        //返回之前key對應的舊值value
        return previous;
    }

    //根據maxSize來調整內存cache的大小,如果maxSize傳入-1,則清空緩存中的所有對象
    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) {
                    break;
                }
                // 取出 map 中第一個元素
                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++;//回收個數+1
            }

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

put() 方法其實重點就在於 trimToSize() 方法裏面,這個方法的作用就是判斷加入元素後是否超過最大緩存數,如果超過就清除掉最少使用的元素。

4、remove 方法

    //從內存緩存中根據key值移除某個對象並返回該對象
    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;
    }

三、總結

  • 爲什麼LruCache內部原理的實現需要用到LinkHashMap來存儲數據吶?

因爲LinkHashMap內部是一個數組加雙向鏈表的形式來存儲數據,他能夠保證插入時候的數據和取出來的時候數據的順序的一致性。也就是說,我們以什麼樣的順序插入數據,就會以什麼樣的順序取出數據。並且更重要的一點是,當我們通過get方法獲取數據的時候,這個獲取的數據會從隊列中跑到隊列頭來,從而很好的滿足我們LruCache的算法設計思想。

  • LruCache 其實使用了 LinkedHashMap 維護了強引用對象?

總緩存的大小一般是可用內存的 1/8,當超過總緩存大小會刪除最少使用的元素,也就是內部 LinkedHashMap 的頭部元素。
當使用 get() 訪問元素後,會將該元素移動到 LinkedHashMap 的尾部

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章