Glide 系列-3:Glide 緩存的實現原理(4.8.0)

一、在 Glide 中配置緩存的方式

首先,咱們能夠在自定義的 GlideModule 中制定詳細的緩存策略。即在 applyOptions() 中經過直接調用 GlideBuilder 的方法來指定緩存的信息:java

@Override
    public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
        builder.setDiskCache(new InternalCacheDiskCacheFactory(context, DISK_CACHE_DIR, DISK_CACHE_SIZE));
        builder.setMemoryCache(...);
        builder.setDiskCache(...);
        // ... 略
    }
複製代碼

另外,咱們在每一個圖片加載請求中自定義當前圖片加載請求的緩存策略,git

Glide.with(getContext())
        .load("https://3-im.guokr.com/0lSlGxgGIQkSQVA_Ja0U3Gxo0tPNIxuBCIXElrbkhpEXBAAAagMAAFBO.png")
        .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.AUTOMATIC))
        .apply(RequestOptions.skipMemoryCacheOf(false))
        .into(getBinding().iv);
複製代碼

以上是兩個比較經常使用的緩存的配置方式,具體的 API 能夠查看相關的源碼瞭解.github

不論 Glide 仍是其餘的框架的緩存無非就是基於內存的緩存和基於磁盤的緩存兩種,並且緩存的管理算法基本都是 LRU. 針對內存緩存,Android 中提供了 LruCache,筆者在以前的文章中曾經分析過這個框架:算法

《Android 內存緩存框架 LruCache 的源碼分析》緩存

至於磁盤緩存, Glide 和 OkHttp 都是基於 DiskLruCache 進行了封裝。這個框架自己的邏輯並不複雜,只是指定了一系列緩存文件的規則,讀者能夠自行查看源碼學習。本文中涉及上述兩種框架的地方再也不詳細追究緩存框架的源碼。網絡

二、Glide 緩存的源碼分析

2.1 緩存配置

首先, 咱們在 applyOptions() 方法中的配置會在實例化單例的 Glide 對象的時候被調用. 因此, 這些方法的做用範圍是全局的, 對應於整個 Glide. 下面的方法是 RequestBuilderbuild() 方法, 也就是咱們最終完成構建 Glide 的地方. 咱們能夠在這個方法中瞭解 RequestBuilder 爲咱們提供了哪些與緩存相關的方法. 以及默認的緩存配置.數據結構

Glide build(@NonNull Context context) {
    // ... 無關代碼, 略

    if (diskCacheExecutor == null) {
      diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();
    }

    if (memorySizeCalculator == null) {
      memorySizeCalculator = new MemorySizeCalculator.Builder(context).build();
    }

    if (bitmapPool == null) {
      int size = memorySizeCalculator.getBitmapPoolSize();
      if (size > 0) {
        bitmapPool = new LruBitmapPool(size);
      } else {
        bitmapPool = new BitmapPoolAdapter();
      }
    }

    if (arrayPool == null) {
      arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes());
    }

    if (memoryCache == null) { // 默認的緩存配置
      memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
    }

    if (diskCacheFactory == null) {
      diskCacheFactory = new InternalCacheDiskCacheFactory(context);
    }

    if (engine == null) {
      engine = new Engine(/*各類參數*/);
    }

    return new Glide(/*各類方法*/);
  }
複製代碼

這裏咱們對 MemorySizeCalculator 這個參數進行一些說明. 顧名思義, 它是緩存大小的計算器, 即用來根據當前設備的環境計算可用的緩存空間 (主要針對的時基於內存的緩存).app

MemorySizeCalculator(MemorySizeCalculator.Builder builder) {
    this.context = builder.context;

    arrayPoolSize =
        isLowMemoryDevice(builder.activityManager)
            ? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR
            : builder.arrayPoolSizeBytes;
    // 計算APP可申請最大使用內存,再乘以乘數因子,內存太低時乘以0.33,通常狀況乘以0.4
    int maxSize =
        getMaxSize(
            builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);

    // ARGB_8888 ,每一個像素佔用4個字節內存
    // 計算屏幕這麼大尺寸的圖片佔用內存大小
    int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;
    // 計算目標位圖池內存大小
    int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);
    // 計算目標Lrucache內存大小,也就是屏幕尺寸圖片大小乘以2
    int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens);
    // 最終APP可用內存大小
    int availableSize = maxSize - arrayPoolSize;
    if (targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) {
      // 若是目標位圖內存大小+目標Lurcache內存大小小於APP可用內存大小,則OK
      memoryCacheSize = targetMemoryCacheSize;
      bitmapPoolSize = targetBitmapPoolSize;
    } else {
      // 不然用APP可用內存大小等比分別賦值
      float part = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens);
      memoryCacheSize = Math.round(part * builder.memoryCacheScreens);
      bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens);
    }
  }
複製代碼

2.2 內存緩存

對於, 每一個加載請求時對應的 DiskCacheStrategy 的設置, 咱們以前的文章中已經提到過它的做用位置, 你能夠參考以前的文章瞭解,框架

《Glide 系列-2:主流程源碼分析(4.8.0)》ide

DiskCacheStrategy 的做用位置剛好也是 Glide 的緩存最初發揮做用的地方, 即 Engine 的 load() 方法. 這裏咱們只保留了與緩存相關的邏輯, 從下面的方法中也能夠看出, 當根據各個參數構建了用於緩存的鍵以後前後從兩個緩存當中加載數據, 拿到了數據以後就進行回調, 不然就須要從原始的數據源中加載數據.

public <R> LoadStatus load(/*各類參數*/) {
    // 根據請求參數獲得緩存的鍵
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);

    // 檢查內存中弱引用是否有目標圖片
    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable); // 1
    if (active != null) {
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      return null;
    }

    // 檢查內存中Lrucache是否有目標圖片
    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable); // 2
    if (cached != null) {
      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
      return null;
    }

    // ...內存中沒有圖片構建任務往下執行, 略

    return new LoadStatus(cb, engineJob);
  }
複製代碼

這裏存在兩個方法,即 1 處的從弱引用中獲取緩存數據,以及 2 處的從內存緩存中獲取緩存數據。它們二者之間有什麼區別呢?

  1. 弱引用的緩存會在內存不夠的時候被清理掉,而基於 LruCache 的內存緩存是強引用的,所以不會由於內存的緣由被清理掉。LruCache 只有當緩存的數據達到了緩存空間的上限的時候纔會將最近最少使用的緩存數據清理出去。
  2. 兩個緩存的實現機制都是基於哈希表的,只是 LruCahce 除了具備哈希表的數據結構還維護了一個鏈表。而弱引用類型的緩存的鍵與 LruCache 一致,可是值是弱引用類型的。
  3. 除了內存不夠的時候被釋放,弱引用類型的緩存還會在 Engine 的資源被釋放的時候清理掉。
  4. 基於弱引用的緩存是一直存在的,沒法被用戶禁用,但用戶能夠關閉基於 LruCache 的緩存。
  5. 本質上基於弱引用的緩存與基於 LruCahce 的緩存針對於不一樣的應用場景,弱引用的緩存算是緩存的一種類型,只是這種緩存受可用內存的影響要大於 LruCache.

接下來讓咱們先看下基於弱引用的緩存相關的邏輯,從上面的 1 處的代碼開始:

// Engine#loadFromActiveResources
  private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }
    EngineResource<?> active = activeResources.get(key); // 1
    if (active != null) {
      active.acquire(); // 2
    }
    return active;
  }

  // ActiveResources#get()
  EngineResource<?> get(Key key) {
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    if (activeRef == null) {
      return null;
    }
    EngineResource<?> active = activeRef.get();
    if (active == null) {
      cleanupActiveReference(activeRef); // 3
    }
    return active;
  }

  // ActiveResources#cleanupActiveReference()
  void cleanupActiveReference(@NonNull ResourceWeakReference ref) {
    activeEngineResources.remove(ref.key);
    if (!ref.isCacheable || ref.resource == null) { // 4
      return;
    }
    EngineResource<?> newResource =
        new EngineResource<>(ref.resource, /*isCacheable=*/ true, /*isRecyclable=*/ false);
    newResource.setResourceListener(ref.key, listener);
    listener.onResourceReleased(ref.key, newResource); // 5
  }

  // Engine#onResourceReleased()
  public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    Util.assertMainThread();
    activeResources.deactivate(cacheKey);
    if (resource.isCacheable()) {
      cache.put(cacheKey, resource); // 將數據緩存到 LruCahce
    } else {
      resourceRecycler.recycle(resource);
    }
  }
複製代碼

這裏的 1 處會先調用 ActiveResources 的 get() 從弱引用中拿數據。當拿到了數據以後調用 acquire() 方法將 EngineResource 的引用計數加 1. 當這個資源被釋放的時候,又會將引用計數減 1(參考 EngineResource 的 release() 方法).

當發現了弱引用中引用的 EngineResource 不存在的時候會在 3 處執行一次清理的邏輯。並在 5 處調用回調接口將弱引用中緩存的數據緩存到 LruCache 裏面。

這裏在將數據緩存以前會先在 4 處判斷緩存是否可用。這裏使用到了 isCacheable 這個字段。經過查看源碼咱們能夠追蹤到這個字段最初傳入的位置是在 RequestOptions 裏面。也就是說,這個字段是針對一次請求的,咱們能夠在構建 Glide 請求的時候經過 apply() 設置這個參數的值(這個字段默認是 true,也就是默認是啓用內存緩存的)。

Glide.with(getContext())
    .load("https://3-im.guokr.com/0lSlGxgGIQkSQVA_Ja0U3Gxo0tPNIxuBCIXElrbkhpEXBAAAagMAAFBO.png")
    .apply(RequestOptions.skipMemoryCacheOf(false)) // 不忽略內存緩存,即啓用
    .into(getBinding().iv);
複製代碼

2.3 磁盤緩存

上面介紹了內存緩存,下面咱們分析一下磁盤緩存。

正如咱們最初的示例那樣,咱們能夠經過在構建請求的時候指定緩存的策略。咱們的圖片加載請求會獲得一個 RequestOptions,咱們經過查看該類的代碼也能夠看出,默認的緩存策略是 AUTOMATIC 的。

這裏的 AUTOMATIC 定義在 DiskCacheStrategy 中,除了 AUTOMATIC 還有其餘幾種緩存策略,那麼它們之間又有什麼區別呢?

  1. ALL:既緩存原始圖片,也緩存轉換事後的圖片;對於遠程圖片,緩存 DATARESOURCE;對於本地圖片,只緩存 RESOURCE
  2. AUTOMATIC (默認策略):嘗試對本地和遠程圖片使用最佳的策略。當你加載遠程數據(好比,從 URL 下載)時,AUTOMATIC 策略僅會存儲未被你的加載過程修改過 (好比,變換、裁剪等) 的原始數據(DATA),由於下載遠程數據相比調整磁盤上已經存在的數據要昂貴得多。對於本地數據,AUTOMATIC 策略則會僅存儲變換過的縮略圖(RESOURCE),由於即便你須要再次生成另外一個尺寸或類型的圖片,取回原始數據也很容易。
  3. DATA:只緩存未被處理的文件。個人理解就是咱們得到的 stream。它是不會被展現出來的,須要通過裝載 decode,對圖片進行壓縮和轉換,等等操做,獲得最終的圖片才能被展現。
  4. NONE:表示不緩存任何內容。
  5. RESOURCE:表示只緩存轉換事後的圖片(也就是通過decode,轉化裁剪的圖片)。

那麼這些緩存的策略是在哪裏使用到的呢?回顧上一篇文章,首先,咱們是在 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:
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
  }

  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);
    }
  }
複製代碼

首先會根據當前所處的階段 current 以及緩存策略判斷應該使用哪一個 DataFetcherGenerator 加載數據。咱們分別來看一下它們:

首先是 ResourceCacheGenerator,它用來從緩存中獲得變換以後數據。當從緩存中拿數據的時候會調用到它的 startNext() 方法以下。從下面的方法也能夠看出,當從緩存中拿數據的時候會先在代碼 1 處構建一個用於獲取緩存數據 key。在構建這個 key 的時候傳入了圖片大小、變換等各類參數,即根據各類變換後的條件獲取緩存數據。所以,這個類是用來獲取變換以後的緩存數據的。

public boolean startNext() {
    List<Key> sourceIds = helper.getCacheKeys();
    if (sourceIds.isEmpty()) {
      return false;
    }
    List<Class<?>> resourceClasses = helper.getRegisteredResourceClasses();
    if (resourceClasses.isEmpty()) {
      if (File.class.equals(helper.getTranscodeClass())) {
        return false;
      }
    }
    while (modelLoaders == null || !hasNextModelLoader()) {
      resourceClassIndex++;
      if (resourceClassIndex >= resourceClasses.size()) {
        sourceIdIndex++;
        if (sourceIdIndex >= sourceIds.size()) {
          return false;
        }
        resourceClassIndex = 0;
      }

      Key sourceId = sourceIds.get(sourceIdIndex);
      Class<?> resourceClass = resourceClasses.get(resourceClassIndex);
      Transformation<?> transformation = helper.getTransformation(resourceClass);
      currentKey =
          new ResourceCacheKey( // 1 構建獲取緩存信息的鍵
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
      cacheFile = helper.getDiskCache().get(currentKey); // 2 從緩存中獲取緩存信息
      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++); // 3 使用文件方式從緩存中讀取緩存數據
      loadData = modelLoader.buildLoadData(cacheFile,
          helper.getWidth(), helper.getHeight(), helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }

    return started;
  }
複製代碼

當找到了緩存的值以後會使用 File 類型的 ModelLoader 加載數據。這個比較容易理解,由於數據存在磁盤上面,須要用文件的方式打開。

另外,咱們再關注下 2 處的代碼,它會使用 helpergetDiskCache() 方法獲取 DiskCache 對象。咱們一直追蹤這個對象就會找到一個名爲 DiskLruCacheWrapper 的類,它內部包裝了 DiskLruCache。因此,最終從磁盤加載數據是使用 DiskLruCache 來實現的。對於最終使用 DiskLruCache 獲取數據的邏輯咱們不進行說明了,它的邏輯並不複雜,都是單純的文件讀寫,只是設計了一套緩存的規則。

上面是從磁盤讀取數據的,那麼數據又是在哪裏向磁盤緩存數據的呢?

在以前的文章中咱們也分析過這部份內容,即當從網絡中打開輸入流以後會回到 DecodeJob 中,進入下一個階段,並再次調用 SourceGeneratorstartNext() 方法。此時會進入到 cacheData() 方法,並將數據緩存到磁盤上:

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());
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
      helper.getDiskCache().put(originalKey, writer); // 將數據緩存到磁盤上面
    } finally {
      loadData.fetcher.cleanup();
    }

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

而後構建一個 DataCacheGenerator 再從磁盤上面讀取出緩存的數據,顯示到控件上面。

還有一個問題,從上文中咱們也能夠看出 Glide 在進行緩存的時候能夠緩存轉換以後的數據,也能夠緩存原始的數據。咱們能夠經過構建的用於獲取緩存的鍵看出這一點:在 ResourceCacheGenerator 中獲取轉換以後的緩存數據的時候,咱們使用 ResourceCacheKey 並傳入了各類參數構建了緩存的鍵;在將數據存儲到磁盤上面的時候咱們使用的是 DataCacheKey,而且沒有傳入那麼多參數。這說明獲取的和存儲的並非同一份數據,那麼轉換以後的數據是在哪裏緩存的呢?

咱們經過查找類 ResourceCacheKey 將位置定位在了 DecodeJobonResourceDecoded() 方法中:

<Z> Resource<Z> onResourceDecoded(DataSource dataSource, Resource<Z> decoded) {
    // ... 略

    Resource<Z> result = transformed;
    boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);
    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 中,隨後會將其緩存到磁盤上面
      deferredEncodeManager.init(key, encoder, lockedResult);
      result = lockedResult;
    }
    return result;
  }
複製代碼

顯然,這裏會根據緩存的策略構建兩種不一樣的 key,並將其傳入到 deferredEncodeManager 中。而後將會在 DecodeJobnotifyEncodeAndRelease() 方法中調用 deferredEncodeManagerencode() 方法將數據緩存到磁盤上:

void encode(DiskCacheProvider diskCacheProvider, Options options) {
      try {
        // 將數據緩存到磁盤上面
        diskCacheProvider.getDiskCache().put(key,
            new DataCacheWriter<>(encoder, toEncode, options));
      } finally {
        toEncode.unlock();
      }
    }
複製代碼

以上就是 Glide 的磁盤緩存的實現原理。

三、總結

在這篇文中咱們在以前的兩篇文章的基礎之上分析了 Glide 的緩存的實現原理。

首先 Glide 存在兩種內存緩存,一個基於弱引用的,一個是基於 LruCache 的。二者存在一些不一樣,在文中咱們已經總結了這部份內容。

而後,咱們分析了 Glide 的磁盤緩存的實現原理。Glide 的磁盤緩存使用了策略模式,存在 4 種既定的緩存策略。Glide 不只能夠原始的數據緩存到磁盤上面,還能夠將作了轉換以後的數據緩存到磁盤上面。它們會基於自身的緩存方式構建不一樣的 key 而後底層使用 DiskLruCache 從磁盤種獲取數據。這部分的核心代碼在 DecodeJob 和三個 DataFetcherGenerator 中。

以上就是 Glide 緩存的全部實現原理。

Glide 系列文章:

  1. Glide 系列-1:預熱、Glide 的經常使用配置方式及其原理
  2. Glide 系列-2:主流程源碼分析(4.8.0)
  3. Glide 系列-3:Glide 緩存的實現原理(4.8.0)

若是你喜歡這篇文章,請點贊哦!你也能夠在如下平臺關注我哦:

全部的文章維護在:Gihub: Android-notes

相關文章
相關標籤/搜索