Glide源碼分析(六),緩存架構(gòu)粒褒、存取命中分析

原文:https://blog.csdn.net/nbsp22/article/details/80666291

分析Glide緩存策略识颊,我們還得從之前分析的Engine#load方法入手,這個方法中奕坟,展示了緩存讀取的一些策略祥款,我們繼續(xù)貼上這塊代碼。

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);
  }

涉及到的緩存類型如下:

內(nèi)存和磁盤各自的兩種緩存

  1. ActiveResources緩存和MemoryCache月杉,MemoryCache我們很好理解刃跛,就是Resouce在內(nèi)存中的緩存,ActiveResources是什么意思呢沙合,其實我們可以這樣理解,類似多級緩存的概念跌帐,當然這里不是特別的適合首懈,ActiveResources緩存和MemoryCache是同時存在的。ActiveResources緩存存放的是所有未被clear的Request請求到的Resource谨敛,這部分Resource會存放至ActiveResources緩存中究履,當Request被clear的時候,會把這部分在ActiveResources緩存中的Resource移動至MemoryCache中去脸狸,只有MemoryCache中能夠命中最仑,則這部分resource又會從MemoryCache移至ActiveResources緩存中去,到這里炊甲,相信大家能夠明白ActiveResources了泥彤,其實相當于是對內(nèi)存緩存再次做了一層,能夠有效的提高訪問速度卿啡,避免過多的操作MemoryCache吟吝,因為我們知道,MemoryCache中存放的緩存可能很多颈娜,這樣的話剑逃,直接在上面做一層ActiveResources緩存顯得就很有必要了浙宜。
  2. DiskCache,磁盤緩存比較簡單蛹磺,其中也分為ResourceCacheKey與DataCacheKey粟瞬,一個是已經(jīng)decode過的可以之間供Target給到View去渲染的,另一個是還未decode過的萤捆,緩存的是源數(shù)據(jù)裙品。磁盤緩存的保存是在第一次請求網(wǎng)絡成功時候,會刷新磁盤緩存鳖轰,此時處理的是源數(shù)據(jù)清酥,至于是否會緩存decode過后的數(shù)據(jù),取決于DiskCacheStrategy的策略蕴侣。

結(jié)合前面所有文章焰轻,這里我再次簡要梳理下資源加載的過程。

簡要資源加載全過程

  1. 檢查ActiveResources緩存中能否命中昆雀,若命中辱志,則請求完成,通知Target渲染對應的View狞膘。若未命中揩懒,則進入Step2。
  2. 檢查MemoryCache緩存能否命中挽封,若命中已球,則請求完成,通知Target渲染對應的View辅愿。若未命中智亮,則進入Step3。
  3. 構(gòu)造或復用已有的EngineJob與DecodeJob点待,開始資源的加載阔蛉,加載過程是ResourceCacheGenerator -> DataCacheGenerator -> SourceGenerator優(yōu)先級順序,不管哪種方式取到了數(shù)據(jù)癞埠,最終都會回調(diào)至DecodeJob中處理状原,區(qū)別在于SourceGenerator會更新磁盤緩存,此時的是DataCacheKey類型的緩存苗踪。進入步驟4颠区。
    4. DecodeJob回調(diào)中,一方面通過decodeFromData從DataFetcher中decode取到的原數(shù)據(jù)通铲,轉(zhuǎn)換為View能夠展示的Resource瓦呼,比如Drawable或Bitmap等,同時根據(jù)緩存策略,取決是否會構(gòu)建ResourceCacheKey類型的緩存央串。decode這一步就已經(jīng)結(jié)束磨澡,接下來會進行線程切換,最終切換到EngineJob的handleResultOnMainThread方法中质和,在這個方法中稳摄,會根據(jù)resource資源,構(gòu)建一個非常重要的角色EngineResource饲宿,它是用來存放至ActiveResources緩存和MemoryCache中的厦酬,這里往ActiveResources緩存中put資源就是在此時回調(diào)至Engine的onEngineJobComplete中完成的。接下來就是回調(diào)至SingleRequest中的onResourceReady中去更新Target中View的渲染資源了瘫想。至此仗阅,全過程就已經(jīng)結(jié)束。

內(nèi)存緩存的要點

相信到這里国夜,有同學已經(jīng)意識到减噪,這里并沒有更新MemoryCache呢,難道此時不正是應該更新到內(nèi)存緩存中去嗎车吹?這里什么時候一個資源才會put至MemoryCache呢筹裕,回到ActiveResources緩存中存放的EngineResource,它內(nèi)部維護了一個計數(shù)窄驹,當計數(shù)減為0的時候朝卒,會觸發(fā)一個callback,它里面的實現(xiàn)就是將EngineResource從ActiveResources緩存移動至MemoryCache乐埠,也就是put到MemoryCache的時機抗斤,為什么是這樣呢?通過我仔細的細節(jié)分析丈咐,每一個加載的SingleRequest中有一個對應的EngineResource的引用瑞眼,SingleRequest是與生命周期綁定的,當所屬的請求上下文被onDestroy是扯罐,會通過其對應的RequestManager取消其所有的Request對象负拟,而在Request的clear中則會調(diào)用Resource的recycle方法烦衣。此時就是EngineResource的recycle方法歹河,因此,當生命周期onDestory被觸發(fā)時花吟,對應EngineResource計數(shù)會減為0秸歧,也就觸發(fā)將EngineResource從ActiveResources緩存移動至MemoryCache。此時ActiveResources緩存會失效衅澈,同時我們可以看到MemoryCache命中時键菱,恰恰會進行一個反向的操作,將EngineResource從MemoryCache重新移動至ActiveResources緩存今布。這里相信大家更明白了经备,為什么這里做了一個類似內(nèi)存的二級緩存拭抬,也是Glide處于一種優(yōu)化的考慮吧。下面我們再來分析下磁盤緩存DataCacheKey命中的情況侵蒙。

磁盤緩存的命中

Glide源碼分析(五)造虎,EngineJob與DecodeJob代碼詳細加載過程一文中,我們看到資源加載成功緩存到磁盤上是在SourceGenerator#cacheData方法中進行的纷闺,我們來看其具體實現(xiàn)算凿。

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);
}

這段代碼邏輯相關比較好理解,根據(jù)loadData中的sourceKey以及簽名信息犁功,構(gòu)造一個DataChcheKey類型的對象氓轰,而后將其put至磁盤緩存中,其中sourceKey就是我們加載資源的GlideUrl對象(https://p.upyun.com/docs/cloud/demo.jpg)浸卦。
磁盤緩存的具體實現(xiàn)我們已經(jīng)了解署鸡,默認是由DiskLruCacheWrapper實現(xiàn),具體功能就是將數(shù)據(jù)寫入預先設置的緩存目錄的文件下镐躲,以文件的方式存放储玫。在分析D加載資源的詳細過程中,我們知道Engine#load會先在內(nèi)存中查找是否有緩存命中萤皂,否則會啟動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;
  }

我們假定內(nèi)存緩存以及在激活的資源池中均沒有命中,則此時會根據(jù)GlideUrl[https://p.upyun.com/docs/cloud/demo.jpg] 以它和簽名組成的DataCacheKey入录,從DiskCache中去尋找這個緩存文件蛤奥,DiskLruCacheWrapper#get方法實現(xiàn)如下:

@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;
 }

可以看到,真正去根據(jù)key獲取文件信息實際上是由getDiskCache().get方法去實現(xiàn)的僚稿,這里我們需要分析getDiskCache()的實現(xiàn)凡桥,也就是操作磁盤文件的類了。

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

getDiskCache的實現(xiàn)也很明確蚀同,就是調(diào)用DiskLruCache的靜態(tài)open方法缅刽,創(chuàng)建一個diskLruCache單例對象,方法入?yún)irectory表示緩存目錄蠢络,maxSize緩存最大大小衰猛。open的實現(xiàn)如下:

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文件是存在的啡省,因此,接下來調(diào)用readJournal根據(jù)緩存key將索引信息讀入lruEntries中,每一個緩存key對應有一個Entry信息卦睹。Entry中保存緩存文件索引的是cleanFiles畦戒,cleanFiles雖然是一個File數(shù)組,但是目前glide對于這個數(shù)據(jù)的size是恒為1的结序,也就是緩存key,Entry,文件是一個一一對應的關系兢交,這里glide用數(shù)組提供了將來一種可擴展性的預留實現(xiàn)。這樣磁盤緩存索引也就建立完成笼痹。下面繼續(xù)看DiskLruCache#get的實現(xiàn)

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索引中根據(jù)key信息查找,而后將結(jié)果返個DiskLruCacheWrapper凳干,這里我們看到有entry.cleanFiles晴裹,。
entry.cleanFiles也就是對應在DataCacheGenerator中cacheFile的實例救赐。因此整個在磁盤cache中查找文件的過程也就比較清楚了涧团。再次看DataCacheGenerator中的startNext,此時cacheFile能夠命中经磅,因此會觸發(fā)對應的modelLoader去從緩存中加載數(shù)據(jù)泌绣。

總結(jié)

這里我們介紹了內(nèi)存緩存,ActiveResources與MemoryCache的命中情況分析预厌,以及DiskCache的DataCacheKey的命中分析阿迈,DiskCach還有一個關于ResourceCacheKey的情況,相應的代碼在ResourceCacheGenerator中轧叽,我們這里不再研究苗沧,也是一樣的思路。這里再強調(diào)幾點炭晒,DataCacheKey中緩存的是DataFetcher拉取的源數(shù)據(jù)待逞,也就是原始的數(shù)據(jù),ResourceCacheKey則是基于原始數(shù)據(jù)网严,做的一層更精細的緩存识樱,從它們的構(gòu)造方法中我們可以看到。

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

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

正如我們簡單的例子震束,這里DataCacheKey只有網(wǎng)絡的url決定怜庸,也即是一個數(shù)據(jù)流對象,不同的decode可以來擴展它驴一,ResourceCacheKey就是這樣一種緩存休雌。至此灶壶,對于Glide的緩存架構(gòu)我們就分析完了肝断,整個系列差不多也接近尾聲了,后面文章中,我會整理一些大綱的總線胸懈,供大家自己研讀担扑。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市趣钱,隨后出現(xiàn)的幾起案子涌献,更是在濱河造成了極大的恐慌,老刑警劉巖首有,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件燕垃,死亡現(xiàn)場離奇詭異,居然都是意外死亡井联,警方通過查閱死者的電腦和手機卜壕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烙常,“玉大人轴捎,你說我怎么就攤上這事〔显啵” “怎么了侦副?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長驼鞭。 經(jīng)常有香客問我秦驯,道長,這世上最難降的妖魔是什么挣棕? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任汇竭,我火速辦了婚禮,結(jié)果婚禮上穴张,老公的妹妹穿的比我還像新娘细燎。我一直安慰自己,他們只是感情好皂甘,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布玻驻。 她就那樣靜靜地躺著,像睡著了一般偿枕。 火紅的嫁衣襯著肌膚如雪璧瞬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天渐夸,我揣著相機與錄音嗤锉,去河邊找鬼。 笑死墓塌,一個胖子當著我的面吹牛瘟忱,可吹牛的內(nèi)容都是我干的奥额。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼访诱,長吁一口氣:“原來是場噩夢啊……” “哼垫挨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起触菜,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤九榔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后涡相,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哲泊,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年催蝗,在試婚紗的時候發(fā)現(xiàn)自己被綠了攻旦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡生逸,死狀恐怖牢屋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情槽袄,我是刑警寧澤烙无,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站遍尺,受9級特大地震影響截酷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜乾戏,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一迂苛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鼓择,春花似錦三幻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至摆出,卻和暖如春朗徊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背偎漫。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工爷恳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人象踊。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓温亲,卻偏偏與公主長得像棚壁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子铸豁,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容