开源框架 | Glide 的三级缓存

说到缓存,都会想到内存缓存 LruCache 和磁盘缓存 DiskLruCache,两者都是基于 LRU(Lest Resently Used)算法并使用 LinkedHashMap 实现的,不同的是前者是保存在内存中,后者是保存在磁盘文件中。Glide也不例外照样使用了这两种缓存,本文不讲 LruCache 和 DiskLruCache 具体的实现原理,从写入和读取缓存的角度解析 Glide 的缓存策略。Glide 默认是使用内存缓存,如果要使用磁盘缓存或者屏蔽内存缓存可以如下设置:

   RequestOptions options = new RequestOptions()
            .skipMemoryCache(true) //屏蔽内存缓存
            .diskCacheStrategy(DiskCacheStrategy.ALL); //使用磁盘缓存
   Glide.with(fragment)
            .load(url)
            .apply(options)
            .into(imageView);
DiskCacheStrategy 的4个抽象方法:
  public abstract boolean isDataCacheable(DataSource dataSource);
  public abstract boolean isResourceCacheable(boolean isFromAlternateCacheKey,
      DataSource dataSource, EncodeStrategy encodeStrategy);
  public abstract boolean decodeCachedResource();
  public abstract boolean decodeCachedData();
  • isDataCacheable()
    返回true,表示缓存原始的没有进行修改过的图片;
  • isResourceCacheable()
    返回true,表示缓存最终转化的图片;
  • decodeCacheResource()
    返回ture,表示解码缓存的最终转化的图片;
  • decodeCacheData()
    返回true,表示解码缓存的没有进行修改过的图片;
DiskCacheStrategy 有5种缓存类型:
  • ALL
    远程图片资源使用 DATA 和 RESOURCE 缓存,本地图片只使用 RESOURCE 缓存;
  • NONE
    不缓存任何图片资源;
  • DATA
    将检索到的原始图片资源(解码之前的)写入磁盘缓存;
  • RESOURCE
    将检索到的原始图片资源解码之后(压缩或转换)再写入磁盘缓存;
  • AUTOMATIC
    根据图片源自动选取缓存策略,数据源可以是:DataFetcher、EncodeStrategy、ResourceEncoder;

1. 内存缓存

Glide 的内存缓存中有两级,一部分是弱引用缓存,一部分是 LruCache,弱引用缓存使用 WeakReference 修饰引用的图片,用于缓存正在使用中的图片;LruCache 就是常见的内存缓存,保存当前应用使用过但不是正在使用中的图片。

1.1 缓存读取

前面分析图片请求流程时,在 Egine.load() 方法内忽略了缓存部分,回到里面:

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

图片进行网络请求前首先会调用 loadFromActiveResources() 获取应用正在使用的资源,资源是保存在一个弱引用(WeakReference)的 HashMap 里:
如果获取到的资源为空,接着调用 loadFromCache() 从内存缓存 LruCache 中获取并移除,然后保存在前面正在使用的弱引用的 HashMap 中,LruCache 的原理是使用一个 LinkedHashMap 对请求过的图片进行保存。

两种情况下获取到的资源不为空时都会调用 SingleRequest.onResourceReady() 通知主线程更新UI。

首先来看 loadFromActiveResources(),这里是从正在使用的图片资源中获取,如果我们请求的图片之前请求过并且正在使用中,那么这个方法就可以拿到这个图片资源:

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

第一步先判断 isMemeoryCacheable,由我们设置 RequestOptions 的 skipMemoryCache(true) 决定,为 true 表示不使用内存缓存 isMemeoryCacheable 就为 false,直接返回 null;为 false 表示使用内存缓存,Glide 默认是使用内存缓存的,此时 isMemeoryCacheable 为 true,接着调用 ActiveResources.get() 方法通过 key 获取图片资源:

  final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();

  EngineResource<?> get(Key key) {
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    if (activeRef == null) {
      return null;
    }

    EngineResource<?> active = activeRef.get();
    if (active == null) {
      cleanupActiveReference(activeRef);
    }
    return active;
  }

拿到图片的 key 从 activeEngineResources 中获取一个弱引用的对象,activeEngineResources 是一个 HashMap,保存的是我们正在使用的图片资源,由于是弱引用的对象,只要系统发起 GC 操作,这些对象都会被回收掉。这就是弱引用缓存,一方面提高了请求效率,另一方面也避免了应用由于图片资源过大导致的内存溢出问题。

如果弱引用缓存中没有我们想要请求的图片,接着就调用 loadFromCache() 去内存缓存 LruCache 中查找,也就是说需要请求的这个图片是否之前使用过但现在没有使用了,Glide 便将其保存在 LruCache 中:

  private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      cached.acquire();
      activeResources.activate(key, cached); //缓存正在使用的图片
    }
    return cached;
  }

  private EngineResource<?> getEngineResourceFromCache(Key key) {
    Resource<?> cached = cache.remove(key); //移除并返回 LreCache 中的图片资源

    final EngineResource<?> result;
    if (cached == null) {
      result = null;
    } else if (cached instanceof EngineResource) {
      result = (EngineResource<?>) cached;
    } else {
      result = new EngineResource<>(cached, true /*isMemoryCacheable*/, true /*isRecyclable*/);
    }
    return result;
  }

重点看 cache.remove(key),cache 就是 LruCache,调用 remove() 获取缓存并移除,拿到资源后如果不为空,由于应用此刻正好需要使用这个资源,首先先将它保存在正在使用的资源中,调用 activeResources.activate(key, cached) 将这个资源保存在我们前面提到的 HashMap 中,最后再返回给 Engine.load();

由此可见,通常情况下 Glide 是有两级缓存的,弱引用缓存和内存缓存,正在使用的图片保存在弱引用的 HashMap 中,使用过但现在不使用的图片保存在 LruCache 的 LinkedHashMap 中,两者之间存在交互的,如果现在请求的图片存在于 LreCache 中,Glide 会将这张图片从 LruCache 中移除并保存在弱引用缓存 activeResources 中,如果正在使用的图片现在不使用了(图片的引用计数为0)Glide 又会将这张图片从 activeResources 中移除并存入 LruCache 中。

Glide 获取一张图片时,首先会从弱引用缓存中获取,没有则从内存缓存 LruCache 中获取,如果有磁盘缓存,接着去磁盘中获取,最后才是通过网络获取。

1.2 缓存写入

前面我们了解了 Glide 在请求一个图片时缓存的获取原理,而缓存又是在什么时候写入的呢?前一章了解到,当我们完成一次图片的请求时,通过 Handler 发送消息给主线程,最终会调用到 EngineJob 的 handleResultOnMainThread() 方法,进去看看都做了什么:

  void handleResultOnMainThread() {
    ...
    engineResource = engineResourceFactory.build(resource, isCacheable);
    hasResource = true;
    engineResource.acquire(); //关注点1
    listener.onEngineJobComplete(this, key, engineResource); //关注点2
    for (int i = 0, size = cbs.size(); i < size; i++) {
      ResourceCallback cb = cbs.get(i);
      if (!isInIgnoredCallbacks(cb)) {
        engineResource.acquire();
        cb.onResourceReady(engineResource, dataSource);
      }
    }
    // Our request is complete, so we can release the resource.
    engineResource.release(); //关注点3

    release(false /*isRemovedFromQueue*/);
  }

先来看关注点2,这里又回到了 Engine 的 onEngineJobComplete() 方法中:

  public void onEngineJobComplete(EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    Util.assertMainThread();
    if (resource != null) {
      resource.setResourceListener(key, this);
      if (resource.isCacheable()) {
        activeResources.activate(key, resource);
      }
    }
    jobs.removeIfCurrent(key, engineJob);
  }

  void activate(Key key, EngineResource<?> resource) {
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key,
            resource,
            getReferenceQueue(),
            isActiveResourceRetentionAllowed);
    ResourceWeakReference removed = activeEngineResources.put(key, toPut); //传入弱引用缓存
    if (removed != null) {
      removed.reset();
    }
  }

很明显,如果 resource 不为空调用 activeResources.activate(),这个方法就是将我们这里的 resource 存入了弱引用缓存中。

那么内存缓存又是什么时候写入的呢?
回到 handleResultOnMainThread() 里面,关注点1和关注点3分别调用了 engineResource 的 acquire() 和 release() 方法,acquire() 每调用一次引用计数 acquired 加1,release() 方法每调用一次 acquired 减1:

  void acquire() {
    ...
    ++acquired;
  }

  void release() {
    ...
    if (--acquired == 0) {
      listener.onResourceReleased(key, this);
    }
  }

引用计数 acquired 表示当前正在使用资源的使用者数,大于0表示资源正在使用中,值为0表示没有使用者使用此刻就需要将它写入内存缓存中,release() 中调用 onResourceReleased() 将没有使用的资源写入内存缓存,仍然又回到了 Engine 中的 onResourceReleased() 方法:

  private final MemoryCache cache;

  public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    Util.assertMainThread();
    activeResources.deactivate(cacheKey); //先从弱引用缓存中移除
    if (resource.isCacheable()) {
      cache.put(cacheKey, resource); //写入内存缓存
    } else {
      resourceRecycler.recycle(resource);
    }
  }

由于这个图片当前已没有使用者,调用 activeResources.deactivate() 先把它从弱引用缓存中清除,然后就是将数据写入内存缓存,cache 是 MemoryCache 类型的,MemoryCache 是一个接口类它的实现者就是 LruCache。

由此可见,正在使用的图片使用 activeResources 以弱引用的方式保存起来,Glide 给图片设置了一个引用计数变量 acquired 用于统计图片当前的引用数,acquired 为0即为图片没有使用者,就将图片从弱引用缓存中移除然后保存到 LruCache 中。

2. 磁盘缓存

2.1 磁盘缓存读取

回到之前讲解请求图片流程,DecodeJob 的 run() 方法调用了 runWrapped(),之前只考虑了第一次请求图片的情况,如果是第二次请求图片,进入 runWrapper() 看看是怎么处理的:

  private void runWrapped() {
    switch (runReason) {
      case INITIALIZE:
        stage = getNextStage(Stage.INITIALIZE); //关注点1
        currentGenerator = getNextGenerator(); //关注点2
        runGenerators(); //关注点3
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }

还是 INITIALIZE,首先来看关注点1,调用 getNextStage 获取下一步的操作标记:

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

当前阶段为:INITIALIZE,首先判断是否需要解码磁盘缓存中经过压缩或者转换的图片,需要则返回 RESOURCE_CACHE,否则判断是否要解码磁盘缓存中原始的图片,即未经过压缩和转换的图片,需要则返回 DATA_CACHE,如果两种都不需要则返回 SOURCE 表示直接请求原始图片,这里具体是使用哪种缓存取决于我们在设置 RequestOptions 的diskCacheStrategy(DiskCacheStrategy.ALL) 时决定;

接着来看关注点2,假设我们通过 getNextStage() 拿到的 stage 为 RESOURCE_CACHE,那么进入 getNextGenerator() 中就会返回一个 ResourceCacheGenerator 对象:

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

ResourceCacheGenerator 对象有什么作用呢?继续来看关注点3,调用 runGenerators(),紧接着调用 currentGenerator.startNext(),这里就是执行这个 ResourceCacheGenerator 的地方,进入 ResourceCacheGenerator 的 startNext():

  public boolean startNext() {
    ...
      currentKey =
          new ResourceCacheKey
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions()); //创建图片资源的key
      cacheFile = helper.getDiskCache().get(currentKey); //获取磁盘缓存
    ... //没有磁盘缓存,
    return started;
  }

首先为当前图片创建一个用于标识图片唯一性的 key,接着很明显了,调用 helper.getDiskCache().get(currentKey) 就是从我们的磁盘缓存中获取图片资源,由于这里是 ResourceCacheGenerator 的 startNext(),所以获取到的资源是经过压缩或者转换后的图片。

要获取未经过压缩及转换的图片的话如何获取呢?首先需要我们在设置 RequestOptions 的 diskCacheStrategy() 时设置的 DiskCacheStrategy 类型是可以缓存原始图片的(ALL、DATA、AUTOMATIC 都可以缓存原始图片),接着 Glide 内部通过调用 DataCacheGenerator 的 startNext() 方法就能获取到原始的图片。

总结下来,当原始图片缓存和修改后的图片缓存都存在时,首先会获取经过压缩或转换后的图片,然后才去获取原始图片,毕竟原始图片一般比经过处理后的图片大且占据更多内存空间,在使用的时候应避免缓存以及从缓存中获取原始的图片。

2.2 磁盘缓存写入
  • 缓存原始图片

磁盘缓存的写入是在请求图片的时候写入的,在磁盘获取的时候 getNextGenerator() 如果返回的是 SourceGenerator 时,表明需要去请求图片,进入 SourceGenerator 的 startNext() 方法:

  public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data); //关键点
    }
  ...
  }

在图片请求之前调用了 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);
  }

调用 helper.getDiskCache().put(originalKey, writer) 将数据写入了磁盘缓存,但这个方法是在 dataToCache 不为空时调用的,dataToCache 是在哪里存值的呢?还是 startNext() 方法里,接着调用loadData.fetcher.loadData(),由于我们使用的是从网络获取图片,所以fetcher 就是 HttpUrlFetcher,也就是调用了 HttpUrlFetcher 的 loadData() :

  public void loadData(@NonNull Priority priority,
      @NonNull DataCallback<? super InputStream> callback) {
    long startTime = LogTime.getLogTime();
    try {
      InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders()); //进行网络请求
      callback.onDataReady(result); //调用 SourceGenerator的onDataReady()方法
    } 
    ...
  }

从网络请求图片资源,请求完成后将请求结果传给了 SourceGenerator 的 onDataReday():

  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data; //关注点1
      cb.reschedule();
    } else {
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher, 
          loadData.fetcher.getDataSource(), originalKey); //关注点2
    }
  }

看关注点1,如果请求返回的数据 data 不为空且需要缓存原始数据,就将 data 赋值给我们刚才提到的 dataToCache,接着调用 cb.reschedule() 会再一次进入到 SourceGenerator 的 startNext() 方法,这个时候 dataToCache 已经不为空就可以写入磁盘缓存了,注意这里是缓存的原始未经过任何修改的图片,如果不需要缓存原始数据,直接调用 DecodeJob.onDataFetcherReady()。

  • 缓存编码(压缩或转换)后的图片

什么时候缓存压缩或转换后的图片呢?cacheData() 方法里面原始图片写入磁盘缓存完成后新建了一个 DataCacheGenerator,然后有如下流程:DataCacheGenerator.startNext() -> HttpUrlFetcher.loadData() -> DataCacheGenerator.onDataReady() -> DecodeJob.onDataFetcherReady() -> decodeFromRetrievedData() -> notifyEncodeAndRelease() -> DeferredEncodeManager.encode(),进入 encode() 方法看看:

    void encode(DiskCacheProvider diskCacheProvider, Options options) {
      GlideTrace.beginSection("DecodeJob.encode");
      try {
        diskCacheProvider.getDiskCache().put(key,
            new DataCacheWriter<>(encoder, toEncode, options)); //写入磁盘缓存
      } finally {
        toEncode.unlock();
        GlideTrace.endSection();
      }
    }

代码写的很清楚了,这里就把编码后的数据写入了磁盘缓存中。

3. 总结

到此为此就介绍完了 Glide 的两种缓存的读取和写入原理,需要注意的是内存缓存不仅是 LruCache 还提供了一种弱引用缓存,用于缓存正在使用的图片资源,由于是弱引用的缓存当系统发起 GC 时就会被回收掉,有效避免了内存较低时系统开启 GC 回收部分内存时可能发生的内存泄漏问题,以及由于图片过大导致内存不足时可能引发的内存溢出问题。

假设我们在一开始设置了 Glide 支持磁盘缓存,且原图和编码后(即压缩或转换)的图片都要缓存;

  • 缓存读取顺序:

    弱引用缓存 -> LruCache -> DiskLruCache

  • 缓存写入顺序:

    DiskLruCache 缓存原图 -> 弱引用缓存 -> LruCache -> DiskLruCache 缓存编码后的图片

  • 注意:

    弱引用缓存和 LruCache 之间存在缓存的转换关系,图片从正在使用状态转为不使用状态,Glide 将图片从弱引用缓存移除然后缓存到 LruCache 中,假如 LruCache 中的某张图片现在需要使用,则图片从 LruCache 中移除缓存到弱引用缓存中,弱引用缓存中保存的是正在使用的图片。

4. 参考

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