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的缓存架构我们就分析完了,整个系列差不多也接近尾声了,后面文章中,我会整理一些大纲的总线,供大家自己研读。

下一篇 Glide源码分析(七),总纲思路梳理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267