天天看點

Android LruCache 源碼分析0. 前言1. 初始化2. 添加3. 取出總結

0. 前言

學過作業系統這門課的朋友都還記得

LRU

這個算法吧,中文名叫”最近最久未使用”,它是用在頁面置換政策中的一種很巧妙的淘汰算法,而在

Android

中,也有一個緩存淘汰機制用到了它,叫做

LruCache

,它也可以說是一個精妙的設計吧,這篇博文中,筆者将帶領大家剖析它源碼中的精妙之處…

1. 初始化

LruCache

類源碼位于

android.util.LruCache

包下,大家也可以同步閱讀。第一件事,便是看其執行個體化過程,隻有一個帶參數的構造函數,參數的意思是緩存最大支援的記憶體容量,注意哦,不是數量,是占用空間的容量:

public LruCache(int maxSize) {
    if (maxSize <= ) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    // 指派
    this.maxSize = maxSize;
    // 建立 HashMap
    // 參數:初始容量為0;加載因子為0.75;以最近最久未使用排序
    this.map = new LinkedHashMap<K, V>(, f, true);
}
           

它将最大容量賦給了成員變量,還建立了一個

LinkedHashMap

,它是一個循環連結清單,前面兩個參數不要緊,關鍵在最後一個參數,最後一個參數含義是”是否以最近通路的順序排序”,也就是說,如果為

true

,那麼

HashMap

的連結清單将會以最近通路的元素在尾部,很久沒通路的元素在頭部的順序來排序,在

LinkedHashMap

的源碼中可以證明這一說法:

// 調用此函數說明産生一次通路記錄
public V get(Object key) {
    ...
    // 周遊連結清單
    for (...; e != null; e = e.next) {
        K eKey = e.key;
        // 如果找到值
        if (eKey == key || ...) {
            if (accessOrder)
                // 将結點e移至循環連結清單尾部
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        }
    }
    return null;
}

private void makeTail(LinkedEntry<K, V> e) {
    e.prv.nxt = e.nxt;
    e.nxt.prv = e.prv;
    // 将 e 去除
    // --------------------
    // |                  ↓
    // pre      e        nxt
    //  ↑                 |
    //  -------------------

    LinkedEntry<K, V> header = this.header;
    LinkedEntry<K, V> oldTail = header.prv;
    e.nxt = header;
    e.prv = oldTail;
    oldTail.nxt = header.prv = e;
    // 将 e 加在循環連結清單尾部: 
    // Tail(header的前面一個元素) 和 Header 中間
    // -------------- --------------
    // |            ↓ ↓            |
    // tail          e           header
    // ↑            | |            ↑
    // -------------- --------------
    modCount++;
}
           

并且,通過

LinkedHashMap#eldest()

方法可以傳回最老的結點:

public Entry<K, V> eldest() {
    // 頭第一個結點便是最老的節點
    LinkedEntry<K, V> eldest = header.nxt;
    // 如果 header 的下一個結點就是 header 
    // 說明該循環連結清單為空
    return eldest != header ? eldest : null;
}
           

2. 添加

初始化步驟為我們提供了一個可供存儲緩存的循環連結清單,還提供好了

LRU

排序。接下來看看如何添加一個緩存記錄的:

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++; // 放入的次數 調用 remove() 不會減少
        size += safeSizeOf(key, value); // 占用的大小 預設為1 供子類重寫 sizeOf()
        previous = map.put(key, value); // 放入 LinkedHashMap 傳回值表示先前是否存在有記錄
        if (previous != null) { // 如果之前緩存中存在有記錄,那麼并沒有放入
            size -= safeSizeOf(key, previous);  // 是以大小要減少
        }
    }
    if (previous != null) {
        entryRemoved(false, key, previous, value);  // 調用供子類覆寫的方法
    }
    trimToSize(maxSize);    // 重建大小 淘汰舊的元素
    return previous;
}
           

不少的代碼,其實就是兩步,先将緩存結點添加進

LinkedHashMap

,然後調用

trimToSize(maxSize)

檢查大小并淘汰。接下來看看是如何淘汰就節點的:

public void trimToSize(int maxSize) {
    while (true) {
        K key;
        V value;
        synchronized (this) {
            ...
            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++; // 移除的次數
        }
        // 子類重寫方法以實作具體的移除政策
        entryRemoved(true, key, value, null);
    }
}
           

上面兩段代碼都提到了

entryRemoved()

方法,這其實是一個供子類擴充功能的方法,它可以被子類覆寫,比如可以再增加一級磁盤緩存,那麼磁盤上的添加和移除緩存方法就需要子類來重寫了:

// 子類重寫方法 以實作具體的移除政策
// 參數一 表示 是否是被淘汰出去的
// 參數三 表示 移除了一個舊的值 用新的值來替換
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

// 子類重寫方法 以實作具體的添加政策
// 參數一 表示 鍵值對的鍵
protected V create(K key) {
    return null;
}
           

3. 取出

添加好了緩存就可以取出來用了,接下來看是如何取出來的:

public final V get(K key) {
    ...
    V mapValue;
    synchronized (this) {
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++; // 用到的元素數目 越大相當于使用率越高
            return mapValue; // 找到了就直接傳回
        }
        missCount++;    // 沒有找到元素 丢失的緩存數目
    }
    // 如果緩存容器中沒有值 就調用 create() 方法再建立
    V createdValue = create(key);
    if (createdValue == null) {
        return null;    // 建立不成功 傳回 null
    }
    synchronized (this) {
        createCount++; // 建立次數
        mapValue = map.put(key, createdValue);  // 放入緩存 特别注意 傳回值是表示之前是否存在有相同值
        if (mapValue != null) {
            // There was a conflict so undo that last put
            map.put(key, mapValue); // 如果不為空 說明之前就存在 那麼再次放之前的引用就可以了
        } else {
            size += safeSizeOf(key, createdValue);  // 放入成功的話就增加大小空間
        }
    }
    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);   // 調用
        return mapValue;
    } else {
        trimToSize(maxSize);    // 如果放進去了 就得再次淘汰
        return createdValue;
    }
}
           

其實完成的内容也很簡單,就是從

LinkedHashMap

中取出,如果有就傳回,如果沒有就調用子類的

create(key)

方法,最後要記得驗證是否需要淘汰最近最久未使用的元素。

總結

LruCache

的源碼還是很簡單的,但它很巧妙地結合

LinkedHashMap

實作了

LRU

算法,可以說非常值得一讀!

繼續閱讀