Android——LruCache原始碼解析

Zhaoxi_Zhang發表於2019-01-01

以下針對 Android API 26 版本的原始碼進行分析。 在瞭解LruCache之前,最好對LinkedHashMap有初步的瞭解,LruCache的實現主要藉助LinkedHashMapLinkedHashMap的原始碼解析,可閱讀Java——LinkedHashMap原始碼解析

概述

  LruCahce其 Lru 是 Least Recently Used 的縮寫,即最近最少使用,是包含對有限數量值的強引用的快取。每當一個值被訪問,它將被移到隊尾。當快取達到指定的數量時,位於隊頭的值將被移除,並且可能被 GC 回收。如果快取的值包含需要顯式釋放的資源,那麼需要重寫entryRemoved方法。如果 key 對應的快取未命中,通過重寫create方法建立對應的 value。這可以簡化程式碼呼叫:即使存在快取未命中,也允許假設始終返回一個值。   預設情況下,快取大小以條目數量度量。在不同快取物件下,通過重寫sizeOf方法測量 key-value 快取的大小。例如如下的例子,這個快取限制了 4MiB 大小的點陣圖:

int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
    protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount();
    }
}
複製程式碼

  這個類是執行緒安全的,通過在快取上執行同步操作來以原子方式執行多個快取操作:

synchronized (cache) {
    if (cache.get(key) == null) {
        cache.put(key, value);
    }
}
複製程式碼

  這個類不允許空值作為 key 或者 value,對於getputremove方法返回null值是明確的行為:快取中不存在這個鍵。

原始碼分析

主要欄位

    //LruCache 主要藉助 LinkedHashMap 按元素訪問順序的迭代順序(此時 accessOrder = true)來實現
    private final LinkedHashMap<K, V> map;

    /** 不同 key-value 條目下快取的大小,不一定是 key-value 條目的數量 */
    private int size;
    //快取大小的最大值
    private int maxSize;
    
    //儲存的 key-value 條目的個數
    private int putCount;
    //建立 key 對應的 value 的次數
    private int createCount;
    //快取移除的次數
    private int evictionCount;
    //快取命中的次數
    private int hitCount;
    //快取未命中的次數
    private int missCount;
複製程式碼

建構函式

/**
 * maxSize 對於快取沒有重寫 sizeOf 方法的時候,這個數值指定了快取中可以容納的最大條目的數量;
 * 對於其他快取,這是快取中條目大小的最大總和。
 */
public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;

    //指定了雜湊表初始容量為0,負載因子為0.75,迭代順序為按照條目訪問順序
    //因此在有對條目進行訪問的操作的時候,條目都會被放置到隊尾,具體細節詳看 LinkedHashMap 的解析
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
複製程式碼

Size操作

LruCache在預設情況下,size 指的是 key-value 條目的個數,當重寫sizeOf函式時,可以自定義 key-value 條目的單位大小,如概述中點陣圖的例子,其通過重寫sizeOf函式,返回的大小值並非是 1,而是不同Bitmap物件的位元組大小。

/**
 * 以使用者定義的單位返回 key-value 條目的大小
 * 預設實現返回1,因此 size 是條目數,max size是最大條目數
 * 條目的大小在快取中時不得更改
 */
protected int sizeOf(K key, V value) {
    return 1;
}

private int safeSizeOf(K key, V value) {
    int result = sizeOf(key, value);
    if (result < 0) {
        throw new IllegalStateException("Negative size: " + key + "=" + value);
    }
    return result;
}

/**
 * 刪除最舊的條目,直到剩餘條目總數小於等於指定的大小。
 */
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!");
            }

            //雜湊表中條目的大小小於指定的大小即終止
            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);

            //移除元素的次數
            evictionCount++;
        }

        //此處 evicted 為 true,表明是為了騰出空間而進行的刪除條目操作
        entryRemoved(true, key, value, null);
    }
}

/**
 * 調整快取的大小
 */
public void resize(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }

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

複製程式碼

查詢

/**
 * 指定 key 對應的 value 值存在時返回,否則通過 create 方法建立相應的 key-value 對。
 * 如果對應的 value 值被返回,那麼這個 key-value 對將被移到隊尾。
 * 當返回 null 時,表明沒有對應的 value 值並且也無法被建立
 */
public final V get(K key) {
    //快取不允許 key 值為 null,因此對於查詢 null 的鍵可直接丟擲異常
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        mapValue = map.get(key);
        //快取命中
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        //快取未命中
        missCount++;
    }

    /*
     * 嘗試建立一個 value 值,這可能需要花費較長的時間完成,當 create 返回時,雜湊表可能變得不同
     * 如果在 create 工作時向雜湊表新增了一個衝突的值(key 已經有對應的 value 值,但 create 方法返回了一個不同的 value 值)
     * 那麼將該值保留在雜湊表中並釋放建立的值。
     */
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    synchronized (this) {
        //快取建立的次數
        createCount++;
        mapValue = map.put(key, createdValue);

        if (mapValue != null) {
            // mapValue 不為 null,說明存在一個衝突值,保留之前的 value 值
            map.put(key, mapValue);
        } else {
            size += safeSizeOf(key, createdValue);
        }
    }

    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        trimToSize(maxSize);
        return createdValue;
    }
}

/**
 * 在快取未命中之後呼叫以計算相應 key 的 value。
 * 當能計算 key 對應的 value 時,返回 value,否則返回 null。預設實現一律返回 null 值。
 *
 * 這個方法在被呼叫的時候沒有新增額外的同步操作,因此其他執行緒可能在這個方法執行時訪問快取
 *
 * 如果 key 對應的 value 儲存在快取中,那麼通過 create 建立的 value 將通過 entryRemoved 方法釋放。
 * 這種情況主要發生在:當多個執行緒同時請求相同的 key (導致建立多個值)時,或者當一個執行緒呼叫 put 而另一個執行緒為其建立值時
 */
protected V create(K key) {
    return null;
}
複製程式碼

儲存

/**
 * 對於 key,快取其相應的 value,key-value 條目放置於隊尾
 *
 * @return 返回先前 key 對應的 value 值
 */
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) {
        //evicted 為 true,表明不是為了騰出空間而進行的刪除操作
        entryRemoved(false, key, previous, value);
    }

    trimToSize(maxSize);
    return previous;
}
複製程式碼

刪除

/**
 * 刪除 key 對應的條目
 *
 * 返回 key 對應的 value值
 */
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;
}

/**
 * 當條目需要被移除或刪除時呼叫
 * 當一個值被移除以騰出空間,通過呼叫 remove 刪除,或者被 put 呼叫替換值時,會呼叫此方法。預設實現什麼也不做
 *
 * 這個方法在被呼叫的時候沒有新增額外的同步操作,因此其他執行緒可能在這個方法執行時訪問快取
 *
 * @param evicted true 表明條目正在被刪除以騰出空間,false 表明刪除是由 put 或 remove 引起的(並非是為了騰出空間)
 *
 * @param newValue key 的新值。如果非 null,則此刪除是由 put 引起的。否則它是由 remove引起的
 */
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
複製程式碼

相關文章