【Glide】快取實現-記憶體和磁碟快取

語言: CN / TW / HK

簡介

本文主要是初步瞭解下 Glide 的快取方式及實現,主要分為以下幾個部分:

  1. Glide 的快取方式介紹
  2. 記憶體與磁碟快取原理
  3. 快取演算法的具體實現

Glide 的快取方式介紹

Glide 快取方式主要有兩種,即記憶體快取和磁碟快取。記憶體快取方面,根據資料是否當前正在使用,又可以細分為 Active 和 非 Active。磁碟快取方面,根據是否進行過形變,又可以細分為 Resource Cache「指經過形變後的快取」 和 Data Cache「指源資料快取」。這裡先簡單瞭解記憶體和磁碟快取。 ```java //GlideBuilder.java @NonNull Glide build(@NonNull Context context) { //省略部分程式碼... //記憶體快取的實現 if (memoryCache == null) { memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize()); } //磁碟快取的工廠實現 if (diskCacheFactory == null) { diskCacheFactory = new InternalCacheDiskCacheFactory(context); }

if (engine == null) {
  engine =
      new Engine(
          memoryCache,
          diskCacheFactory,
          diskCacheExecutor,
          sourceExecutor,
          GlideExecutor.newUnlimitedSourceExecutor(),
          animationExecutor,
          isActiveResourceRetentionAllowed);
}
    //省略部分程式碼...
return new Glide(
    context,
    engine,
    memoryCache,
    bitmapPool,
    arrayPool,
    requestManagerRetriever,
    connectivityMonitorFactory,
    logLevel,
    defaultRequestOptionsFactory,
    defaultTransitionOptions,
    defaultRequestListeners,
    experiments);

} / memeory cache 的實現是 LruResourceCache, 在建立後會作為引數分別傳入 Engine 和 Glide 物件。 disk cache factory 的實現是 InternalCacheDiskCacheFactory, 在建立後只作為引數傳入 Engine 物件。 通過工廠最終建立的預設磁碟快取是 DiskLruCacheWrapper。 / ```

記憶體與磁碟快取原理

從 LruResourceCache 和 DiskLruCacheWrapper 的實現上來說,其快取原理都採用了 LRU 演算法,結合原始碼實現具體點來看,

首先來看下記憶體快取原理, java public class LruResourceCache extends LruCache<Key, Resource<?>> implements MemoryCache { //MemoryCache 定義了記憶體快取的行為,具體哪些行為呢? /* 1.快取操作:put(Key key, Resource<?> resource) 2.刪除操作:remove(Key key) 3.清空操作:clearMemory() */ //LruCache<K, V> 是 LRU 的實現類,內部通過 LinkedHashMap 實現快取資料的維護, //該類已經實現了 put, get, remove, clear 等操作,作為子類, //要實現 getSize(Y item) 方法來指定單個快取資料的大小。LruCache 這個類的方法實現是關鍵。 //作為 LruResourceCache 本身,剩下需要做的事情就不多了。 } 再來看下磁碟快取原理, ```java public class DiskLruCacheWrapper implements DiskCache { //DiskCache 定義了磁碟快取的行為,同 MemeoryCache 類似,它也定義了一些操作方法 / 1.快取操作:put(Key key, Writer writer) 2.刪除操作:delete(Key key) 3.清空操作:clear() 4.獲取操作:get(Key key),這步在 MemoryCache 裡沒有定義,但在 LruCache 裡是定義並實現的。 除了上面的行為操作,該介面還定義了兩個介面, 一個是用來建立磁碟快取的工廠介面,一個是用來快取讀寫操作的 Writer。 / //同 LruCache 類似,這個是磁碟快取的 LRU 實現類。 private DiskLruCache diskLruCache;

private synchronized DiskLruCache getDiskCache() throws IOException { if (diskLruCache == null) { //diskLruCache 物件通過 open 方法建立 diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize); } return diskLruCache; } } ``` DiskLruCache 會比記憶體快取的 LruCache 複雜些,表現在:1.涉及到執行緒池操作,2.涉及到檔案讀寫操作。但雖說有差別,核心點(用 LinkedHashMap 實現快取集合)是一樣的。

快取演算法的具體實現

在分析 Glide 快取演算法實現之前,先簡單瞭解下 LRU 演算法。

LRU(Least Recently Used)該演算法是一種頁面置換演算法,對每個頁面增加一個用來記錄使用時間的欄位,在儲存空間滿的情況下,根據比較使用時間欄位,刪除最近最少使用的頁面,從而用來存放新頁面。

從原理上可知:1.儲存空間是受限的,也就是說有個儲存空間初始值。2.需要一個數據結構能滿足內容替換,也就是找出最近最少使用的元素。

LruCache 的 put 方法實現分析, ```java public class LruCache { private final Map> cache = new LinkedHashMap<>(100, 0.75f, true);

public LruCache(long size) { //構造方法裡做的就是初始化儲存空間元素數量的多少(也就是總共能放多少頁,放滿了就要置換了) //注意!!!這個 size 是儲存空間總大小,單位是和 getSize 是一致的 / Glide 用 MemorySizeCalculator 的 memoryCacheSize 欄位對其進行的賦值,這個值的計算 會根據裝置螢幕尺寸有所不同。單位是 byte / this.initialMaxSize = size; this.maxSize = size; }

@Nullable public synchronized Y put(@NonNull T key, @Nullable Y item) { //通過 getSize 方法獲取到 item 元素的空間大小,以 byte 為單位 final int itemSize = getSize(item); if (itemSize >= maxSize) { //這個的實現交給了子類 onItemEvicted(key, item); return null; }

if (item != null) {
  currentSize += itemSize;
}
//利用 LinkedHashMap 型別的 cache 存入 item 值
//根據返回值判斷 cache 中是否有舊的元素需要去除
@Nullable Entry<Y> old = cache.put(key, 
item == null ? null : new Entry<>(item, itemSize));
if (old != null) {
  currentSize -= old.size;

  if (!old.value.equals(item)) {
    onItemEvicted(key, old.value);
  }
}
//檢驗空間是否足夠,不夠的話進行元素空間置換
evict();

return old != null ? old.value : null;

}

private void evict() { trimToSize(maxSize); }

protected synchronized void trimToSize(long size) { Map.Entry> last; Iterator>> cacheIterator; while (currentSize > size) { cacheIterator = cache.entrySet().iterator(); //這裡直接拿到的 cache 裡的第一個元素, //因為是 LinkedHashMap,這樣一來最近最少使用的就是第一個 //迴圈釋放,直到空間足夠為止 last = cacheIterator.next(); final Entry toRemove = last.getValue(); currentSize -= toRemove.size; final T key = last.getKey(); cacheIterator.remove(); onItemEvicted(key, toRemove.value); } }

//用於包裝儲存元素的靜態類,包含具體的元素及其空間大小 static final class Entry { final Y value; final int size;

@Synthetic
Entry(Y value, int size) {
  this.value = value;
  this.size = size;
}

} } DiskLruCache 的 put 方法實現分析,java public final class DiskLruCache implements Closeable { //在通過 InternalCacheDiskCacheFactory 建立時指定了預設大小 250MB private long maxSize; private final LinkedHashMap lruEntries = new LinkedHashMap(0, 0.75f, true); public final class Editor { private final Entry entry; private final boolean[] written; private boolean committed;

public File getFile(int index) throws IOException {
  synchronized (DiskLruCache.this) {
    if (entry.currentEditor != this) {
        throw new IllegalStateException();
    }
    if (!entry.readable) {
        written[index] = true;
    }
    File dirtyFile = entry.getDirtyFile(index);
    directory.mkdirs();
    return dirtyFile;
  }
}

} } //在磁碟快取處理上,行為操作是通過 DiskLruCacheWrapper 物件操作的,來看下 DiskLruCacheWrapper 類 //DiskLruCacheWrapper public class DiskLruCacheWrapper implements DiskCache { private DiskLruCache diskLruCache;

@Override public void put(Key key, Writer writer) { //SafeKeyGenerator 確保能獲取到一個 safeKey String safeKey = safeKeyGenerator.getSafeKey(key); try { try { DiskLruCache diskCache = getDiskCache(); //通過 diskCache 獲取 safeKey 對應的值 Value current = diskCache.get(safeKey); if (current != null) { return; } //獲取 safeKey 對應的編輯器 DiskLruCache.Editor editor = diskCache.edit(safeKey); try { File file = editor.getFile(0); //通過 Writer 將檔案資料寫入到 file 中 if (writer.write(file)) { //如果寫入成功,就執行 commit editor.commit(); } } } } } ``` DiskLruCache 比 LruCache 的實現會複雜一些,暫且先做簡單瞭解,後續再進行深入分析。