Android 主流開源框架(七)Glide 的緩存機制

前言

最近有個想法——就是把 Android 主流開源框架進行深刻分析,而後寫成一系列文章,包括該框架的詳細使用與源碼解析。目的是經過鑑賞大神的源碼來了解框架底層的原理,也就是作到不只要知其然,還要知其因此然。html

這裏我說下本身閱讀源碼的經驗,我通常都是按照平時使用某個框架或者某個系統源碼的使用流程入手的,首先要知道怎麼使用,而後再去深究每一步底層作了什麼,用了哪些好的設計模式,爲何要這麼設計。前端

系列文章:java

更多幹貨請關注 AndroidNotesgit

上一篇主要講了 Glide 的執行流程,當時是禁用了內存與磁盤緩存的,因此涉及到緩存相關的流程都省略了,還沒看上篇的建議先去看一遍,由於這篇講的緩存機制不少都是要與上篇聯繫起來的。程序員

1、Glide 中的緩存

默認狀況下,Glide 在加載圖片以前會依次檢查是否有如下緩存:github

  1. 活動資源 (Active Resources):正在使用的圖片
  2. 內存緩存 (Memory cache):內存緩存中的圖片
  3. 資源類型(Resource):磁盤緩存中轉換事後的圖片
  4. 數據來源 (Data):磁盤緩存中的原始圖片

也就是說 Glide 中實際有四級緩存,前兩個屬於內存緩存,後兩個屬於磁盤緩存。以上每步是按順序檢查的,檢查到哪一步有緩存就直接返回圖片,不然繼續檢查下一步。若是都沒有緩存,則 Glide 會從原始資源(File、Uri、遠程圖片 url 等)中加載圖片。算法

2、緩存 Key

緩存功能必然要有一個惟一的緩存 Key 用來存儲和查找對應的緩存數據。那麼下面咱們就看下 Glide 的緩存 Key 是怎麼生成的。 其實上一篇文章中已經瞄過一眼了,是在 Engine 類的 load() 方法中生成的:後端

/*Engine*/
  public <R> LoadStatus load( GlideContext glideContext, Object model, Key signature, int width, int height, Class<?> resourceClass, Class<R> transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map<Class<?>, Transformation<?>> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, Options options, boolean isMemoryCacheable, boolean useUnlimitedSourceExecutorPool, boolean useAnimationPool, boolean onlyRetrieveFromCache, ResourceCallback cb, Executor callbackExecutor) {

    EngineKey key =
        keyFactory.buildKey(
            model,
            signature,
            width,
            height,
            transformations,
            resourceClass,
            transcodeClass,
            options);

	 ...

  }
複製代碼

繼續跟進:設計模式

/*EngineKeyFactory*/
  EngineKey buildKey( Object model, Key signature, int width, int height, Map<Class<?>, Transformation<?>> transformations, Class<?> resourceClass, Class<?> transcodeClass, Options options) {
    return new EngineKey(
        model, signature, width, height, transformations, resourceClass, transcodeClass, options);
  }
複製代碼
class EngineKey implements Key {

  ...


  @Override
  public boolean equals(Object o) {
    if (o instanceof EngineKey) {
      EngineKey other = (EngineKey) o;
      return model.equals(other.model)
          && signature.equals(other.signature)
          && height == other.height
          && width == other.width
          && transformations.equals(other.transformations)
          && resourceClass.equals(other.resourceClass)
          && transcodeClass.equals(other.transcodeClass)
          && options.equals(other.options);
    }
    return false;
  }

  @Override
  public int hashCode() {
    if (hashCode == 0) {
      hashCode = model.hashCode();
      hashCode = 31 * hashCode + signature.hashCode();
      hashCode = 31 * hashCode + width;
      hashCode = 31 * hashCode + height;
      hashCode = 31 * hashCode + transformations.hashCode();
      hashCode = 31 * hashCode + resourceClass.hashCode();
      hashCode = 31 * hashCode + transcodeClass.hashCode();
      hashCode = 31 * hashCode + options.hashCode();
    }
    return hashCode;
  }

  ...

}
複製代碼

能夠看到,這裏傳入了 model(File、Uri、遠程圖片 url 等)、簽名、寬高(這裏的寬高是指顯示圖片的 View 的寬高,不是圖片的寬高)等參數,而後經過 EngineKeyFactory 構建了一個 EngineKey 對象(即緩存 Key),而後 EngineKey 經過重寫 equals() 與 hashCode() 方法來保證緩存 Key 的惟一性。緩存

雖然決定緩存 Key 的參數不少,可是加載圖片的代碼寫好後這些參數都是不會變的。不少人遇到的 「服務器返回的圖片變了,可是前端顯示的仍是之前的圖片」 的問題就是這個緣由,由於雖然服務器返回的圖片變了,可是圖片 url 仍是之前那個,其餘決定緩存 Key 的參數也不會變,Glide 就認爲有該緩存,就會直接從緩存中獲取,而不是從新下載,因此顯示的仍是之前的圖片。

對於這個問題,有幾種方法能夠解決,分別以下:
(1)圖片 url 不要固定 也就是說若是某個圖片改變了,那麼該圖片的 url 也要跟着改變。

(2)使用 signature() 更改緩存 Key 咱們剛剛知道了決定緩存 Key 的參數包括 signature,恰好 Glide 提供了 signature() 方法來更改該參數。具體以下:

Glide.with(this).load(url).signature(new ObjectKey(timeModified)).into(imageView);
複製代碼

其中 timeModified 能夠是任意數據,這裏用圖片的更改時間。例如圖片改變了,那麼服務器應該改變該字段的值,而後隨圖片 url 一塊兒返回給前端,這樣前端加載的時候就知道圖片改變了,須要從新下載。

(3)禁用緩存 前端加載圖片的時候設置禁用內存與磁盤緩存,這樣每次加載都會從新下載最新的。

Glide.with(this)
        .load(url)
        .skipMemoryCache(true) // 禁用內存緩存
        .diskCacheStrategy(DiskCacheStrategy.NONE) // 禁用磁盤緩存
        .into(imageView);
複製代碼

以上 3 種方法均可以解決問題,可是推薦使用第一種,這樣設計是比較規範的,後臺人員就應該這麼設計。第二種方法也能夠,可是這樣無疑是給後端、前端人員都增長了麻煩。第三種是最不推薦的,至關於捨棄了緩存功能,每次都要從服務器從新下載圖片,不只浪費用戶流量,並且每次加載須要等待也影響用戶體驗。

3、緩存策略

在講 Glide 中的內存緩存與磁盤緩存以前,咱們先了解下緩存策略。例如加載一張圖片顯示到設備上的緩存策略應該這樣設計:

當程序第一次從網絡上加載圖片後,就將它緩存到設備磁盤中,下次使用這張圖片的時候就不用再從網絡上加載了。爲了提高用戶體驗,每每還會在內存中緩存一份,由於從內存中加載圖片比從磁盤中加載要快。程序下次加載這張圖片的時候首先從內存中查找,若是沒有就去磁盤中查找,都沒有才從網絡上加載。

這裏的緩存策略涉及到緩存的添加、獲取和刪除操做,何時進行這些操做等邏輯就構成了一種緩存算法。目前經常使用的一種緩存算法是 LRU(Least Recently Used),即最近最少使用算法。它的核心思想是當緩存滿時,會優先淘汰那些最近最少使用的緩存對象。採用 LRU 算法的緩存有兩種:LruCache 和 DiskLruCache,LruCache 用於實現內存緩存,DiskLruCache 則用於實現磁盤緩存,二者結合使用就能夠實現上面的緩存策略。

LruCache 和 DiskLruCache 的內部算法原理是採用一個 LinkedHashMap 以強引用的方式存儲外界的緩存對象,其提供了 get() 和 put() 方法來完成緩存的獲取和添加的操做。當緩存滿時,會移除較早使用的緩存對象,而後再添加新的緩存對象。能夠用以下流程圖表示:

下面要講的 Glide 中的內存緩存與磁盤緩存也是用的 LruCache 和 DiskLruCache,只不過 LruCache 用的不是 SDK 中的,而是本身寫的,可是看了原理實際上是同樣的。而 DiskLruCache 用的是 JakeWharton 封裝的 DiskLruCache

4、內存緩存

Glide 默認是配置了內存緩存的,固然 Glide 也提供了 API 給咱們開啓和禁用,以下:

// 開啓內存緩存
Glide.with(this).load(url).skipMemoryCache(false).into(imageView);
// 禁用內存緩存
Glide.with(this).load(url).skipMemoryCache(true).into(imageView);
複製代碼

文章開頭說了,Glide 在加載圖片以前會依次檢查四級緩存。如今緩存 Key 也拿到了,那麼咱們先看看前兩級中的內存緩存是怎麼獲取的(下面分析的時候須要用默認加載語句或者手動開啓內存緩存)。從上一篇文章知道,獲取內存緩存的代碼也是在 Engine 類的 load() 方法中,咱們進去看看:

/*Engine*/
  public <R> LoadStatus load(...) {

    // 構建緩存 Key
    EngineKey key =
        keyFactory.buildKey(
            model,
            signature,
            width,
            height,
            transformations,
            resourceClass,
            transcodeClass,
            options);

    EngineResource<?> memoryResource;
    synchronized (this) {
	  // 從內存中加載緩存數據
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

	  ...

    }

	// 加載完成回調
    cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
    return null;
  }
複製代碼

繼續點擊 loadFromMemory() 方法進去看看:

/*Engine*/
  private EngineResource<?> loadFromMemory(
      EngineKey key, boolean isMemoryCacheable, long startTime) {
    if (!isMemoryCacheable) {
      return null;
    }

    //(1)
    EngineResource<?> active = loadFromActiveResources(key);
    if (active != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return active;
    }

    //(2)
    EngineResource<?> cached = loadFromCache(key);
    if (cached != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return cached;
    }

    return null;
  }
複製代碼

這裏我標記了兩個關注點,分別以下:

  • (1):表示從 ActiveResources 中加載緩存數據。
  • (2):表示從內存緩存中加載緩存數據。

是的,這就是 Glide 四級緩存中的前兩級。ActiveResources 裏面主要包含了一個 HashMap 的相關操做,而後 HashMap 中保存的值又是弱引用來引用的,也就是說這裏是採用一個弱引用的 HashMap 來緩存活動資源。下面咱們分析下這兩個關注點:

  • Engine#loadFromMemory() 中的關注點(1)

咱們點擊關注點(1)看看:

/*Engine*/
  private EngineResource<?> loadFromActiveResources(Key key) {
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {
      active.acquire();
    }

    return active;
  }
複製代碼

繼續看 get() 方法:

/*ActiveResources*/
  synchronized EngineResource<?> get(Key key) {
	// 從 HashMap 中獲取 ResourceWeakReference
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    if (activeRef == null) {
      return null;
    }

    // 從弱引用中獲取活動資源
    EngineResource<?> active = activeRef.get();
    if (active == null) {
      cleanupActiveReference(activeRef);
    }
    return active;
  }
複製代碼

能夠看到,這裏首先從 HashMap 中獲取 ResourceWeakReference(繼承了弱引用),而後從弱引用中獲取了活動資源(獲取活動資源),即正在使用的圖片。也就是說正在使用的圖片實際是經過弱引用維護,而後保存在 HashMap 中的。

繼續看 acquire() 方法:

/*EngineResource*/
  synchronized void acquire() {
    if (isRecycled) {
      throw new IllegalStateException("Cannot acquire a recycled resource");
    }
    ++acquired;
  }
複製代碼

發現這裏是將 acquired 變量 +1,這個變量用來記錄圖片被引用的次數。該變量除了 acquire() 方法中作了 +1 操做,還在 release() 方法中作了 -1 的操做,以下:

/*EngineResource*/
  void release() {
    boolean release = false;
    synchronized (this) {
      if (acquired <= 0) {
        throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
      }
      if (--acquired == 0) {
        release = true;
      }
    }
    if (release) {
      listener.onResourceReleased(key, this);
    }
  }

  /*Engine*/
  @Override
  public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    activeResources.deactivate(cacheKey);
    if (resource.isMemoryCacheable()) {
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource, /*forceNextFrame=*/ false);
    }
  }

  /*ActiveResources*/
  synchronized void deactivate(Key key) {
    ResourceWeakReference removed = activeEngineResources.remove(key);
    if (removed != null) {
      removed.reset();
    }
  }
複製代碼

能夠看到,當 acquired 減到 0 的時候,又回調了 Engine#onResourceReleased()。在 onResourceReleased() 方法中首先將活動資源從弱引用的 HashMap 中移除(清理活動資源),而後將它緩存到內存緩存中(存儲內存緩存)

也就是說,release() 方法主要是釋放資源。當咱們從一屏滑動到下一屏的時候,上一屏的圖片就會看不到,這個時候就會調用該方法。還有咱們關閉當前顯示圖片的頁面時會調用 onDestroy() 方法,最終也會調用該方法。這兩種狀況很明顯是不須要用到該圖片了,那麼理所固然的會調用 release() 方法來釋放弱引用的 HashMap 中緩存的活動資源。

這樣也就實現了正在使用中的圖片使用弱引用來進行緩存,不在使用中的圖片使用 LruCache 來進行緩存的功能。

  • Engine#loadFromMemory() 中的關注點(2)

咱們點擊關注點(2)看看:

/*Engine*/
  private EngineResource<?> loadFromCache(Key key) {
	//(2.1)
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
	  //(2.2)
      cached.acquire();
	  //(2.3)
      activeResources.activate(key, cached);
    }
    return cached;
  }
複製代碼

這裏我標註了 3 個關注點,分別以下:

  • (2.1):這裏是獲取內存緩存。點進去看看:
/*Engine*/
  private EngineResource<?> getEngineResourceFromCache(Key key) {
    Resource<?> cached = cache.remove(key);

    final EngineResource<?> result;
    if (cached == null) {
      result = null;
    } else if (cached instanceof EngineResource) {
      // Save an object allocation if we've cached an EngineResource (the typical case).
      result = (EngineResource<?>) cached;
    } else {
      result =
          new EngineResource<>(
              cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
    }
    return result;
  }
複製代碼

能夠看到,這裏的 cache 就是 LruResourceCache,remove() 操做就是移除緩存的同時獲取該緩存(獲取內存緩存)。LruResourceCache 繼承了 LruCache,雖然不是 SDK 中的 LruCache,可是看了原理實際上是同樣的,也就是說內存緩存使用的是 LRU 算法實現的。

  • (2.2):與關注點(1)中的獲取活動資源同樣,也是將 acquired 變量 +1,而後用來記錄圖片被引用的次數。
  • (2.3):將內存中獲取的緩存數據緩存到弱引用的 HashMap 中。

再回去看我標記了高亮的文字,發現這 2 個關注點主要作了獲取活動資源、清理活動資源、獲取內存緩存、存儲內存緩存。其中清理內存緩存的操做 LRU 算法已經自動幫咱們實現了,那是否是發現少了存儲活動資源的步驟?

活動資源是哪裏來的呢?其實就是咱們從網絡請求中返回的數據。從上一篇文章(能夠回去搜索 onEngineJobComplete() 回憶下上下文)能夠知道網絡請求回來後先進行解碼,而後在 Engine#onEngineJobComplete() 方法中進行了活動資源的存儲,我再貼下代碼:

/*Engine*/
  @Override
  public synchronized void onEngineJobComplete( EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    // A null resource indicates that the load failed, usually due to an exception.
    if (resource != null && resource.isMemoryCacheable()) {
      activeResources.activate(key, resource);
    }

    jobs.removeIfCurrent(key, engineJob);
  }

  /*ActiveResources*/
  synchronized void activate(Key key, EngineResource<?> resource) {
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);

	// 存儲活動資源
    ResourceWeakReference removed = activeEngineResources.put(key, toPut);
    if (removed != null) {
      removed.reset();
    }
  }
複製代碼

以上就是 Glide 內存緩存的原理,可是咱們發現除了利用 LruCache 實現的內存緩存,還有一個是利用弱引用的 HashMap 實現的。通常若是讓咱們設計,可能就只會想到用 LruCache 實現內存緩存。那這裏設計多一個弱引用的 HashMap 的好處是什麼呢?

郭霖的深刻探究Glide的緩存機制中是這樣描述的使用activeResources來緩存正在使用中的圖片,能夠保護這些圖片不會被LruCache算法回收掉。我以爲這樣解釋不太合理,我看完源碼並無以爲這個弱引用的 HashMap 起到了 「保護圖片不被 LRU 算法回收」 的做用。我以爲有以下做用(若有不對請指出):
(1)提升訪問效率
由於 ActiveResources 用的是 HashMap,而 LruCache 用的是 LinkedHashMap,而且在實例化 LinkedHashMap 時是設置了訪問順序的(以下設置),因此 HashMap 的訪問速度是要比 LinkedHashMap 快的。

// accessOrder 設置爲 true,表示是訪問順序模式
Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
複製代碼

(2)防止內存泄漏
ActiveResources 中的 HashMap 是弱引用維護的,而 LruCache 中的 LinkedHashMap 用的是強引用。由於弱引用對象會隨時被 gc 回收,因此能夠防止內存泄漏。這裏列舉下各類引用的區別:

  • 強引用: 直接的對象引用。
  • 軟引用: 當一個對象只有軟引用存在時,系統內存不足時此對象會被 gc 回收。
  • 弱引用: 當一個對象只有弱引用存在時, 此對象會隨時被 gc 回收。

5、磁盤緩存

5.1 磁盤緩存策略

前面說了禁用緩存只須要以下設置便可:

Glide.with(this).load(url).diskCacheStrategy(DiskCacheStrategy.NONE).into(imageView);
複製代碼

上面的 DiskCacheStrategy 封裝的是磁盤緩存策略,一共有以下幾種策略:

  1. ALL:既緩存原始圖片,也緩存轉換事後的圖片。
  2. NONE:不緩存任何內容。
  3. DATA:只緩存原始圖片。
  4. RESOURCE:只緩存轉換事後的圖片。
  5. AUTOMATIC:默認策略,它會嘗試對本地和遠程圖片使用最佳的策略。若是是遠程圖片,則只緩存原始圖片;若是是本地圖片,那麼只緩存轉換事後的圖片。

其實 5 種策略總結起來對應的就是文章開頭說的後兩級緩存,即資源類型(Resource)與 數據來源(Data),下面經過源碼來分析下它們是在哪裏獲取、存儲和清理緩存的。

5.2 資源類型(Resource)

該級緩存只緩存轉換事後的圖片,那麼咱們須要先配置以下策略:

Glide.with(this).load(url).diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(imageView);
複製代碼

經過上一篇文章可知,當咱們從主線程切換到子線程去執行請求的時候用到了磁盤緩存策略,那麼咱們這裏直接從 DecodeJob 任務的 run() 方法開始分析:

/*DecodeJob*/
  @Override
  public void run() {

    ...

    try {
	  // 執行
      runWrapped();
    } catch (CallbackException e) {
      throw e;
    }

	...

  }
複製代碼

繼續 runWrapped() 方法:

/*DecodeJob*/
  private void runWrapped() {
    switch (runReason) {
      case INITIALIZE:
	    // 1. 獲取資源狀態
        stage = getNextStage(Stage.INITIALIZE);
		// 2. 根據資源狀態獲取資源執行器
        currentGenerator = getNextGenerator();
		// 3. 執行
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }

  /*DecodeJob*/
  private Stage getNextStage(Stage current) {
    switch (current) {
      case INITIALIZE:
        return diskCacheStrategy.decodeCachedResource()
            ? Stage.RESOURCE_CACHE
            : getNextStage(Stage.RESOURCE_CACHE);
      case RESOURCE_CACHE:
        return diskCacheStrategy.decodeCachedData()
            ? Stage.DATA_CACHE
            : getNextStage(Stage.DATA_CACHE);
      case DATA_CACHE:
        // Skip loading from source if the user opted to only retrieve the resource from cache.
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
  }

  /*DecodeJob*/
  private DataFetcherGenerator getNextGenerator() {
    switch (stage) {
      case RESOURCE_CACHE:
        return new ResourceCacheGenerator(decodeHelper, this);
      case DATA_CACHE:
        return new DataCacheGenerator(decodeHelper, this);
      case SOURCE:
        return new SourceGenerator(decodeHelper, this);
      case FINISHED:
        return null;
      default:
        throw new IllegalStateException("Unrecognized stage: " + stage);
    }
  }
複製代碼

這裏會根據緩存策略獲取到資源狀態,而後再根據資源狀態獲取資源執行器,最後調用 runGenerators() 方法:

/*DecodeJob*/
  private void runGenerators() {
    currentThread = Thread.currentThread();
    startFetchTime = LogTime.getLogTime();
    boolean isStarted = false;
    while (!isCancelled
        && currentGenerator != null
        && !(isStarted = currentGenerator.startNext())) {
      stage = getNextStage(stage);
      currentGenerator = getNextGenerator();

      if (stage == Stage.SOURCE) {
        reschedule();
        return;
      }
    }
  }
複製代碼

能夠看到,該方法中會調用當前執行器的 startNext() 方法,由於咱們配置的緩存策略是 RESOURCE,因此這裏直接看 ResourceCacheGenerator 的 startNext() 方法:

/*ResourceCacheGenerator*/
  @Override
  public boolean startNext() {

    ...

    while (modelLoaders == null || !hasNextModelLoader()) {

      ...
		
	  //(1)
      currentKey =
          new ResourceCacheKey(
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
	  //(2)
      cacheFile = helper.getDiskCache().get(currentKey);
      if (cacheFile != null) {
        sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData =
          modelLoader.buildLoadData(
              cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
		//(3)
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }

    return started;
  }
複製代碼

能夠看到,根據我標記的關注點這裏首先構建緩存 Key,而後根據緩存 Key 去獲取緩存文件(獲取轉換後的圖片),最後將緩存文件加載成須要的數據。其中 helper.getDiskCache() 爲 DiskLruCacheWrapper,內部是經過 DiskLruCache 操做的,也就是說這一級的磁盤緩存使用的是 LRU 算法實現的。

由於獲取的是緩存文件,因此這裏的 loadData.fetcher 實際爲 ByteBufferFileLoader,繼續看 ByteBufferFileLoader#(loadData):

/*ByteBufferFileLoader*/
    @Override
    public void loadData( @NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
      ByteBuffer result;
      try {
        result = ByteBufferUtil.fromFile(file);
      } catch (IOException e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "Failed to obtain ByteBuffer for file", e);
        }
        callback.onLoadFailed(e);
        return;
      }

      callback.onDataReady(result);
    }
複製代碼

這裏主要是將緩存文件轉換成 ByteBuffer,而後經過 onDataReady() 方法回調出去,最終回調到 DecodeJob 的 onDataFetcherReady() 方法中,後面的流程就跟上一篇文章差很少了。

上面是獲取緩存的流程,那麼是哪裏存儲緩存的呢?咱們能夠用反推的方法,剛剛獲取緩存 Key 的時候用的是 ResourceCacheKey,那麼存儲緩存與獲取緩存確定都是用的 ResourceCacheKey,通過查找發現除了 ResourceCacheGenerator,只有在 DecodeJob 的 onResourceDecoded() 方法中使用到:

/*DecodeJob*/
  <Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
    
	...

    boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);
    if (diskCacheStrategy.isResourceCacheable(
        isFromAlternateCacheKey, dataSource, encodeStrategy)) {
      if (encoder == null) {
        throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass());
      }
      final Key key;
	  //(1)
      switch (encodeStrategy) {
        case SOURCE:
          key = new DataCacheKey(currentSourceKey, signature);
          break;
        case TRANSFORMED:
          key =
              new ResourceCacheKey(
                  decodeHelper.getArrayPool(),
                  currentSourceKey,
                  signature,
                  width,
                  height,
                  appliedTransformation,
                  resourceSubClass,
                  options);
          break;
        default:
          throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy);
      }

      LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
	  //(2)
      deferredEncodeManager.init(key, encoder, lockedResult);
      result = lockedResult;
    }
    return result;
  }
複製代碼

內部又調用了 init() 方法:

private static class DeferredEncodeManager<Z> {
    private Key key;
    private ResourceEncoder<Z> encoder;
    private LockedResource<Z> toEncode;

    <X> void init(Key key, ResourceEncoder<X> encoder, LockedResource<X> toEncode) {
      this.key = key;
      this.encoder = (ResourceEncoder<Z>) encoder;
      this.toEncode = (LockedResource<Z>) toEncode;
    }

    void encode(DiskCacheProvider diskCacheProvider, Options options) {
      GlideTrace.beginSection("DecodeJob.encode");
      try {
		//(3)
        diskCacheProvider
            .getDiskCache()
            .put(key, new DataCacheWriter<>(encoder, toEncode, options));
      } finally {
        toEncode.unlock();
        GlideTrace.endSection();
      }
    }
}
複製代碼

能夠看到,根據我標記的關注點這裏首先根據緩存策略構建不一樣的緩存 Key,而後調用 DeferredEncodeManager 的 init() 方法給變量 key 賦值,而後 key 又在 encode() 方法中使用了,該方法中就作了存儲緩存的操做(存儲轉換後的圖片)

那麼咱們如今看看 encode() 方法在哪裏被調用了唄,點擊發現只在 DecodeJob 的 notifyEncodeAndRelease() 方法中被調用了:

/*DecodeJob */
  private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) {
    if (resource instanceof Initializable) {
      ((Initializable) resource).initialize();
    }

    Resource<R> result = resource;
    LockedResource<R> lockedResource = null;
    if (deferredEncodeManager.hasResourceToEncode()) {
      lockedResource = LockedResource.obtain(resource);
      result = lockedResource;
    }

    notifyComplete(result, dataSource);

    stage = Stage.ENCODE;
    try {
      if (deferredEncodeManager.hasResourceToEncode()) {
		// 將資源緩存到磁盤
        deferredEncodeManager.encode(diskCacheProvider, options);
      }
    } finally {
      if (lockedResource != null) {
        lockedResource.unlock();
      }
    }
    // Call onEncodeComplete outside the finally block so that it's not called if the encode process
    // throws.
    onEncodeComplete();
  }
複製代碼

notifyEncodeAndRelease() 方法是咱們上一篇文章中講的解碼完成了通知下去的步驟,也就是說第一次加載的時候在 SourceGenerator#startNext() 中請求到數據,而後解碼完成,最後再存儲緩存。

上面已經實現了轉換後的圖片的獲取、存儲,剩下的清理操做 LRU 算法已經自動幫咱們實現了。接下來繼續看下原始圖片是怎麼獲取、存儲與清理的。

5.3 數據來源(Data)

該級緩存只緩存原始圖片,那麼咱們須要先配置以下策略:

Glide.with(this).load(url).diskCacheStrategy(DiskCacheStrategy.DATA).into(imageView);
複製代碼

與資源類型同樣,只不過這裏緩存策略換成了 DATA,因此前面就不講了,咱們直接看 DataCacheGenerator 的 startNext() 方法:

/*DataCacheGenerator*/
  @Override
  public boolean startNext() {
    while (modelLoaders == null || !hasNextModelLoader()) {

      ...

      //(1)
      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
      //(2)
      cacheFile = helper.getDiskCache().get(originalKey);
      if (cacheFile != null) {
        this.sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData =
          modelLoader.buildLoadData(
              cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
		//(3)
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }
複製代碼

能夠看到,根據我標記的關注點這裏首先構建緩存 Key,而後根據緩存 Key 去獲取緩存文件(獲取原始圖片),最後將緩存文件加載成須要的數據。與資源類型同樣,這裏的 helper.getDiskCache() 也爲 DiskLruCacheWrapper,因此這一級的磁盤緩存使用的也是 LRU 算法實現的。

這裏獲取的一樣是緩存文件,因此這裏的 loadData.fetcher 也爲 ByteBufferFileLoader,最終也是回調到 DecodeJob 的 onDataFetcherReady() 方法中。

那麼是哪裏存儲緩存的呢?一樣用反推的方法,可是發現除了 DataCacheGenerator 還有兩個地方用到了。
第一個與資源類型同樣是在 DecodeJob#onResourceDecoded():

/*DecodeJob*/
  <Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
    
	...

    boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);
	//(1)
    if (diskCacheStrategy.isResourceCacheable(
        isFromAlternateCacheKey, dataSource, encodeStrategy)) {
      if (encoder == null) {
        throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass());
      }
      final Key key;
      switch (encodeStrategy) {
        case SOURCE:
          key = new DataCacheKey(currentSourceKey, signature);
          break;
        case TRANSFORMED:
          key =
              new ResourceCacheKey(
                  decodeHelper.getArrayPool(),
                  currentSourceKey,
                  signature,
                  width,
                  height,
                  appliedTransformation,
                  resourceSubClass,
                  options);
          break;
        default:
          throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy);
      }

      LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
      deferredEncodeManager.init(key, encoder, lockedResult);
      result = lockedResult;
    }
    return result;
  }
複製代碼

這裏的關注點(1)作了一個緩存策略的判斷,由於前面配置的緩存策略是 DATA,因此這裏調用的是 DATA 中的 isResourceCacheable() 方法:

/*DiskCacheStrategy*/
  public static final DiskCacheStrategy DATA =
      new DiskCacheStrategy() {
        @Override
        public boolean isDataCacheable(DataSource dataSource) {
          return dataSource != DataSource.DATA_DISK_CACHE && dataSource != DataSource.MEMORY_CACHE;
        }

        // 調用的是該方法
        @Override
        public boolean isResourceCacheable( boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) {
          return false;
        }

        @Override
        public boolean decodeCachedResource() {
          return false;
        }

        @Override
        public boolean decodeCachedData() {
          return true;
        }
      };
複製代碼

能夠看到,isResourceCacheable() 方法始終返回 false,因此上面關注點(1)是進不去的,能夠排除。

那咱們繼續看下另外一個地方用到的:

/*SourceGenerator*/
  private void cacheData(Object dataToCache) {
    long startTime = LogTime.getLogTime();
    try {
      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
      DataCacheWriter<Object> writer =
          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
	  //(1)
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
	  //(2)
      helper.getDiskCache().put(originalKey, writer);

	  ...

    } finally {
      loadData.fetcher.cleanup();
    }

    sourceCacheGenerator =
        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
  }
複製代碼

這裏首先構建緩存 Key,而後存儲緩存(存儲原始圖片)。而該方法是在 SourceGenerator#startNext() 中調用的:

/*SourceGenerator*/
  @Override
  public boolean startNext() {
	//(1)
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
              || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        startNextLoad(loadData);
      }
    }
    return started;
  }
複製代碼

能夠看到,關注點(1)中首先判斷緩存不爲空才進行緩存數據的操做,那咱們看下 dataToCache 是哪裏被賦值了唄,查找發現只有在 SourceGenerator#onDataReadyInternal() 中賦值過:

/*SourceGenerator*/
  void onDataReadyInternal(LoadData<?> loadData, Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
	  // 賦值
      dataToCache = data;
      // We might be being called back on someone else's thread. Before doing anything, we should
      // reschedule to get back onto Glide's thread.
	  // 回調
      cb.reschedule();
    } else {
      cb.onDataFetcherReady(
          loadData.sourceKey,
          data,
          loadData.fetcher,
          loadData.fetcher.getDataSource(),
          originalKey);
    }
  }
複製代碼

能夠看到,onDataReadyInternal() 方法又是咱們熟悉的,也就是上一篇文章中加載完數據後調用的。上一篇文章是由於禁用了緩存,因此走的是 else。這裏配置的緩存策略是 DATA,因此天然走的是 if。

那麼賦值完成,下一步確定要用到,咱們繼續跟這裏的回調方法,發現調用的是 EngineJob 的 reschedule() 方法:

/*EngineJob*/
  @Override
  public void reschedule(DecodeJob<?> job) {
    getActiveSourceExecutor().execute(job);
  }
複製代碼

這裏又用線程池執行了 DecodeJob,因此最後又回到了 SourceGenerator 的 startNext() 方法,這時候 dataToCache 就不是空了,因此就將數據緩存起來了。其實 cacheData() 方法中存儲緩存的時候還構建了一個 DataCacheGenerator,而後存儲完成又執行了 DataCacheGenerator#startNext(),這裏再從磁盤獲取緩存後纔將圖片顯示到控件上,也就是說網絡請求拿到數據後是先緩存數據,而後再從磁盤獲取緩存才顯示到控件上。

同理,原始圖片的清理操做也是 LRU 算法自動幫咱們實現了。

能夠看到,這裏資源類型(Resource)與數據來源(Data)中存儲緩存的步驟我都是利用緩存 Key 去反推得出數據是哪裏緩存的。有時候適當的利用反推是挺方便的,若是你不習慣利用反推,能夠跟着程序走,也就是網絡請求到數據後再一步步跟着去看是哪裏緩存的。

6、總結

經過分析 Glide 的緩存機制,發現設計的確實精妙。利用四級緩存大大提升了圖片的加載效率,磁盤緩存策略也提高了框架的靈活性,若是讓咱們設計一個圖片加載框架,徹底能夠將 Glide 中的這些優勢用上。

參考資料:

關於我

我是 wildmaCSDN 認證博客專家簡書程序員優秀做者,擅長屏幕適配。 若是文章對你有幫助,點個贊就是對我最大的承認!

相關文章
相關標籤/搜索