美文网首页
开源框架 | Glide 的三级缓存

开源框架 | Glide 的三级缓存

作者: 南子李 | 来源:发表于2020-09-18 13:51 被阅读0次

    说到缓存,都会想到内存缓存 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. 参考

    相关文章

      网友评论

          本文标题:开源框架 | Glide 的三级缓存

          本文链接:https://www.haomeiwen.com/subject/obllyktx.html