1. 程式人生 > >安卓開發學習之LruCache原始碼閱讀

安卓開發學習之LruCache原始碼閱讀

背景

LruCache是最近最久未使用的快取,是安卓系統裡常見的快取策略之一。

 

原始碼閱讀

LruCache類裡定義的屬性如下

    private final LinkedHashMap<K, V> map; // 記憶體物件,雜湊連結串列

    private int size; // 目前使用的記憶體數
    private int maxSize; // 最大記憶體數

    private int putCount; // 新增到map佇列的運算元
    private int createCount; // 新增的空白記憶體的數量
    private int evictionCount; // 被lru演算法刪除(而不是使用者手動呼叫刪除)的記憶體數量
    private int hitCount; // 根據鍵找值時找到的次數
    private int missCount; // 找不到值的次數

原來就是封裝了一個雜湊連結串列,然後記錄各種數量。於是我順著構造方法、增加、查詢、刪除、清空以及其他方法的順序進行原始碼閱讀

構造方法

構造方法程式碼如下,傳入了一個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);
    }

儲存最大記憶體數量,然後例項化map,但是map初始長度是0,增長因子是0.75(也就是當新增一個元素進去時,發現當前連結串列長度達到或超過連結串列最大長度的75%時,會先擴容到原來的兩倍,再新增新元素),第三個引數表示對映連結串列的排序方式,true表示按照訪問順序排列,false表示按照插入順序排序,這裡要實現最近最久未使用,自然要按照訪問順序排列對映連結串列,把不常用的放到連結串列頭部,常用的放到連結串列尾部。

 

新增方法

給快取裡新增元素的方法程式碼如下,實際是對對映連結串列的操作

    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++; // 移入數量+1
            size += safeSizeOf(key, value); // size++
            previous = map.put(key, value); // 把新的鍵值對插入,返回鍵對應的老值
            if (previous != null) {
                size -= safeSizeOf(key, previous); // 如果以前有值,size不應該變化
            }
        }

        if (previous != null) { // 回撥,空實現
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize); // 保證size不大於maxSize
        return previous;
    }

這個方法前面的都比較容易理解,而最後的trimToSize()方法則是用來剔除連結串列首部那些不常用的元素的,剔除的幅度是保證當前對映連結串列長度不大於傳入的引數。我們看一下trimToSize()方法

    public void trimToSize(int maxSize) {
        // 從頭結點開始移除,直到size不大於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!");
                }

                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); // size--
                evictionCount++; // 移除數量++
            }

            entryRemoved(true, key, value, null); // 空實現
        }
    }

 

獲取元素的方法

獲取元素的方法是get()方法,程式碼如下

    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++; // 否則,丟失數++,進入下面的處理
        }


        V createdValue = create(key); // 預設返回null
        if (createdValue == null) {
            return null; // 返回null
        }

        // 如果某個類覆寫了create()方法,讓它返回不是null
        synchronized (this) {
            createCount++; // 創造數+1
            mapValue = map.put(key, createdValue); // 先嚐試把新值插進去,獲得老值

            // 如果老值不為空,就不採用新值,還把老值插回去,否則size++
            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue); // size++
            }
        }

        if (mapValue != null) { // 老值不是null,返回
            entryRemoved(false, key, createdValue, mapValue); // 空實現
            return mapValue;
        } else { // 否則就是插入了新值,返回新值
            trimToSize(maxSize); // 同時執行lru演算法,清除頭結點
            return createdValue;
        }
    }

 

刪除方法

刪除方法是remove(),程式碼如下

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

        if (previous != null) {
            entryRemoved(false, key, previous, null); // 空實現
        }

        return previous;
    }

 

清空方法

清空方法是evictAll()方法,程式碼如下

    public final void evictAll() {
        trimToSize(-1); // -1 will evict 0-sized elements
    }

給trimToSize()傳入的引數是-1,那麼trimToSize()方法會從連結串列首位開始清除,直到對映連結串列的大小是0為止,當然就清空了

 

其他方法

重置大小

resize()方法,程式碼如下

    public void resize(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

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

其實就是改變一下maxSize,然後呼叫trimToSize()方法執行lru以適應新的容量

 

快照方法

snapShot()方法,程式碼如下

    public synchronized final Map<K, V> snapshot() {
        return new LinkedHashMap<K, V>(map);
    }

就是獲取一個map的副本出去

 

結語

安卓裡的LruCache類就是這樣,內部核心是維護了一個對映連結串列,以及實現了一個trimToSize()方法,這個方法也是lru的實現。

以上原始碼來自Android8.0,sdk26