1. 程式人生 > >Glide原始碼分析(六),快取架構、存取命中分析

Glide原始碼分析(六),快取架構、存取命中分析

分析Glide快取策略,我們還得從之前分析的Engine#load方法入手,這個方法中,展示了快取讀取的一些策略,我們繼續貼上這塊程式碼。

Engine#load

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) { Util.assertMainThread(); long
startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0; EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable); if (active != null
) { cb.onResourceReady(active, DataSource.MEMORY_CACHE); if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Loaded resource from active resources", startTime, key); } return null; } EngineResource<?> cached = loadFromCache(key, isMemoryCacheable); if (cached != null) { cb.onResourceReady(cached, DataSource.MEMORY_CACHE); if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Loaded resource from cache", startTime, key); } return null; } EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache); if (current != null) { current.addCallback(cb); if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Added to existing load", startTime, key); } return new LoadStatus(cb, current); } EngineJob<R> engineJob = engineJobFactory.build( key, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache); DecodeJob<R> decodeJob = decodeJobFactory.build( glideContext, model, key, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, onlyRetrieveFromCache, options, engineJob); jobs.put(key, engineJob); engineJob.addCallback(cb); engineJob.start(decodeJob); if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Started new load", startTime, key); } return new LoadStatus(cb, engineJob); }

涉及到的快取型別如下:

記憶體和磁碟各自的兩種快取

  1. ActiveResources快取和MemoryCache,MemoryCache我們很好理解,就是Resouce在記憶體中的快取,ActiveResources是什麼意思呢,其實我們可以這樣理解,類似多級快取的概念,當然這裡不是特別的適合,ActiveResources快取和MemoryCache是同時存在的。ActiveResources快取存放的是所有未被clear的Request請求到的Resource,這部分Resource會存放至ActiveResources快取中,當Request被clear的時候,會把這部分在ActiveResources快取中的Resource移動至MemoryCache中去,只有MemoryCache中能夠命中,則這部分resource又會從MemoryCache移至ActiveResources快取中去,到這裡,相信大家能夠明白ActiveResources了,其實相當於是對記憶體快取再次做了一層,能夠有效的提高訪問速度,避免過多的操作MemoryCache,因為我們知道,MemoryCache中存放的快取可能很多,這樣的話,直接在上面做一層ActiveResources快取顯得就很有必要了。
  2. DiskCache,磁碟快取比較簡單,其中也分為ResourceCacheKey與DataCacheKey,一個是已經decode過的可以之間供Target給到View去渲染的,另一個是還未decode過的,快取的是源資料。磁碟快取的儲存是在第一次請求網路成功時候,會重新整理磁碟快取,此時處理的是源資料,至於是否會快取decode過後的資料,取決於DiskCacheStrategy的策略。

結合前面所有文章,這裡我再次簡要梳理下資源載入的過程。

簡要資源載入全過程

  1. 檢查ActiveResources快取中能否命中,若命中,則請求完成,通知Target渲染對應的View。若未命中,則進入Step2。
  2. 檢查MemoryCache快取能否命中,若命中,則請求完成,通知Target渲染對應的View。若未命中,則進入Step3。
  3. 構造或複用已有的EngineJob與DecodeJob,開始資源的載入,載入過程是ResourceCacheGenerator -> DataCacheGenerator -> SourceGenerator優先順序順序,不管哪種方式取到了資料,最終都會回撥至DecodeJob中處理,區別在於SourceGenerator會更新磁碟快取,此時的是DataCacheKey型別的快取。進入步驟4。
    4. DecodeJob回撥中,一方面通過decodeFromData從DataFetcher中decode取到的原資料,轉換為View能夠展示的Resource,比如Drawable或Bitmap等,同時根據快取策略,取決是否會構建ResourceCacheKey型別的快取。decode這一步就已經結束,接下來會進行執行緒切換,最終切換到EngineJob的handleResultOnMainThread方法中,在這個方法中,會根據resource資源,構建一個非常重要的角色EngineResource,它是用來存放至ActiveResources快取和MemoryCache中的,這裡往ActiveResources快取中put資源就是在此時回撥至Engine的onEngineJobComplete中完成的。接下來就是回撥至SingleRequest中的onResourceReady中去更新Target中View的渲染資源了。至此,全過程就已經結束。

記憶體快取的要點

相信到這裡,有同學已經意識到,這裡並沒有更新MemoryCache呢,難道此時不正是應該更新到記憶體快取中去嗎?這裡什麼時候一個資源才會put至MemoryCache呢,回到ActiveResources快取中存放的EngineResource,它內部維護了一個計數,當計數減為0的時候,會觸發一個callback,它裡面的實現就是將EngineResource從ActiveResources快取移動至MemoryCache,也就是put到MemoryCache的時機,為什麼是這樣呢?通過我仔細的細節分析,每一個載入的SingleRequest中有一個對應的EngineResource的引用,SingleRequest是與生命週期繫結的,當所屬的請求上下文被onDestroy是,會通過其對應的RequestManager取消其所有的Request物件,而在Request的clear中則會呼叫Resource的recycle方法。此時就是EngineResource的recycle方法,因此,當生命週期onDestory被觸發時,對應EngineResource計數會減為0,也就觸發將EngineResource從ActiveResources快取移動至MemoryCache。此時ActiveResources快取會失效,同時我們可以看到MemoryCache命中時,恰恰會進行一個反向的操作,將EngineResource從MemoryCache重新移動至ActiveResources快取。這裡相信大家更明白了,為什麼這裡做了一個類似記憶體的二級快取,也是Glide處於一種優化的考慮吧。下面我們再來分析下磁碟快取DataCacheKey命中的情況。

磁碟快取的命中

Glide原始碼分析(五),EngineJob與DecodeJob程式碼詳細載入過程一文中,我們看到資源載入成功快取到磁碟上是在SourceGenerator#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);
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      Log.v(TAG, "Finished encoding source to cache"
          + ", key: " + originalKey
          + ", data: " + dataToCache
          + ", encoder: " + encoder
          + ", duration: " + LogTime.getElapsedMillis(startTime));
    }
  } finally {
    loadData.fetcher.cleanup();
  }

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

這段程式碼邏輯相關比較好理解,根據loadData中的sourceKey以及簽名信息,構造一個DataChcheKey型別的物件,而後將其put至磁碟快取中,其中sourceKey就是我們載入資源的GlideUrl物件(https://p.upyun.com/docs/cloud/demo.jpg)。
磁碟快取的具體實現我們已經瞭解,預設是由DiskLruCacheWrapper實現,具體功能就是將資料寫入預先設定的快取目錄的檔案下,以檔案的方式存放。在分析D載入資源的詳細過程中,我們知道Engine#load會先在記憶體中查詢是否有快取命中,否則會啟動DecodeJob,在它中總共有三個DataFetchGenerator,這裡和磁碟快取相關的就是DataCacheGenerator,具體邏輯是在其DataCacheGenerator#startNext方法中。

@Override
  public boolean startNext() {
    while (modelLoaders == null || !hasNextModelLoader()) {
      sourceIdIndex++;
      if (sourceIdIndex >= cacheKeys.size()) {
        return false;
      }

      Key sourceId = cacheKeys.get(sourceIdIndex);
      // PMD.AvoidInstantiatingObjectsInLoops The loop iterates a limited number of times
      // and the actions it performs are much more expensive than a single allocation.
      @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
      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;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

我們假定記憶體快取以及在啟用的資源池中均沒有命中,則此時會根據GlideUrl[https://p.upyun.com/docs/cloud/demo.jpg] 以它和簽名組成的DataCacheKey,從DiskCache中去尋找這個快取檔案,DiskLruCacheWrapper#get方法實現如下:

@Override
 public File get(Key key) {
   String safeKey = safeKeyGenerator.getSafeKey(key);
   if (Log.isLoggable(TAG, Log.VERBOSE)) {
     Log.v(TAG, "Get: Obtained: " + safeKey + " for for Key: " + key);
   }
   File result = null;
   try {
     // It is possible that the there will be a put in between these two gets. If so that shouldn't
     // be a problem because we will always put the same value at the same key so our input streams
     // will still represent the same data.
     final DiskLruCache.Value value = getDiskCache().get(safeKey);
     if (value != null) {
       result = value.getFile(0);
     }
   } catch (IOException e) {
     if (Log.isLoggable(TAG, Log.WARN)) {
       Log.w(TAG, "Unable to get from disk cache", e);
     }
   }
   return result;
 }

可以看到,真正去根據key獲取檔案資訊實際上是由getDiskCache().get方法去實現的,這裡我們需要分析getDiskCache()的實現,也就是操作磁碟檔案的類了。

private synchronized DiskLruCache getDiskCache() throws IOException {
   if (diskLruCache == null) {
     diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
   }
   return diskLruCache;
 }

getDiskCache的實現也很明確,就是呼叫DiskLruCache的靜態open方法,建立一個diskLruCache單例物件,方法入參directory表示快取目錄,maxSize快取最大大小。open的實現如下:

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
      throws IOException {
    ...
    // Prefer to pick up where we left off.
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {
      try {
        cache.readJournal();
        cache.processJournal();
        return cache;
      } catch (IOException journalIsCorrupt) {
        System.out
            .println("DiskLruCache "
                + directory
                + " is corrupt: "
                + journalIsCorrupt.getMessage()
                + ", removing");
        cache.delete();
      }
    }

    // Create a new empty cache.
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    cache.rebuildJournal();
    return cache;
  }

我們分析最簡單的情況,如果在磁碟中有快取檔案了,顯然此時if語句journalFile檔案是存在的,因此,接下來呼叫readJournal根據快取key將索引資訊讀入lruEntries中,每一個快取key對應有一個Entry資訊。Entry中儲存快取檔案索引的是cleanFiles,cleanFiles雖然是一個File陣列,但是目前glide對於這個資料的size是恆為1的,也就是快取key,Entry,檔案是一個一一對應的關係,這裡glide用陣列提供了將來一種可擴充套件性的預留實現。這樣磁碟快取索引也就建立完成。下面繼續看DiskLruCache#get的實現

public synchronized Value get(String key) throws IOException {
   checkNotClosed();
   Entry entry = lruEntries.get(key);
   if (entry == null) {
     return null;
   }

   if (!entry.readable) {
     return null;
   }

   ...
   return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
 }

還是分析簡單的情況,這裡就是在Entry索引中根據key資訊查詢,而後將結果返個DiskLruCacheWrapper,這裡我們看到有entry.cleanFiles,。
entry.cleanFiles也就是對應在DataCacheGenerator中cacheFile的例項。因此整個在磁碟cache中查詢檔案的過程也就比較清楚了。再次看DataCacheGenerator中的startNext,此時cacheFile能夠命中,因此會觸發對應的modelLoader去從快取中載入資料。

總結

這裡我們介紹了記憶體快取,ActiveResources與MemoryCache的命中情況分析,以及DiskCache的DataCacheKey的命中分析,DiskCach還有一個關於ResourceCacheKey的情況,相應的程式碼在ResourceCacheGenerator中,我們這裡不再研究,也是一樣的思路。這裡再強調幾點,DataCacheKey中快取的是DataFetcher拉取的源資料,也就是原始的資料,ResourceCacheKey則是基於原始資料,做的一層更精細的快取,從它們的構造方法中我們可以看到。

key =
    new ResourceCacheKey(
        decodeHelper.getArrayPool(),
        currentSourceKey,
        signature,
        width,
        height,
        appliedTransformation,
        resourceSubClass,
        options);

// DataCacheKey
key = new DataCacheKey(currentSourceKey, signature);

正如我們簡單的例子,這裡DataCacheKey只有網路的url決定,也即是一個數據流物件,不同的decode可以來擴充套件它,ResourceCacheKey就是這樣一種快取。至此,對於Glide的快取架構我們就分析完了,整個系列差不多也接近尾聲了,後面文章中,我會整理一些大綱的匯流排,供大家自己研讀。