详解Glide框架(三):请求流程与缓存策略分析

805 阅读8分钟

这是关于Glide框架系统分析的第三篇文章,这里涉及到Glide最最常考的知识原理:缓存机制。这里的考题往往会围绕以下两点来问:

  1. Glide到底有几级缓存?这些缓存都是什么?

  2. 当Glide获取资源后,处理资源的流程是什么样的?

不得不说,这整个流程中发生的事情实在是太多了,看的我有些应接不暇。所以这里我们只围绕着主题来说,目的是能够准确回答上面提出的两个问题即可。下面我们就围绕下图的时序,来分析多级缓存的主要过程。在图中我用三种颜色标记了三个流程步骤,分别是 内存缓存(红色)->磁盘缓存(蓝色)->请求源(绿色)。咱们就逐一探索每一块的流程吧。

内存缓存

内存的缓存主要分为两类,第一类就是通过loadFromActiveResources(key, isMemoryCacheable);方法,于ActiveResources容器结构中获取活跃中的资源。内存缓存的命中流程看起来相对比较简单,在时序图中均以红色呈现。

ActiveResources

该流程过程用序号1标识。

private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {//来自于options中的配置项,是否允许从缓存获取资源。适用于某些特殊场景。
      return null;
    }
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {
      active.acquire();//资源引用自增1
    }

    return active;
  }

若未命中,则通过loadFromCache()方法,从MemoryCache容器中获取缓存的资源。这个MemoryCache如果没有被指定的话,默认是LruResourceCache。它的赋值时机是在Glide初始化时,也就是在调用Glide.with()过程中的前半截:执行GlideBuilder.build的过程中赋值的。从MemoryCache中获取到包装为EngineResource的资源,不过这里用的是remove操作。同时,将该资源的引用计数加1,并将该资源添加到ActiveResources容器中。

private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }

    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      cached.acquire();//命中缓存资源引用自增1
      activeResources.activate(key, cached);//加入到ActiveResources容器。
    }
    return cached;
  }

LruResourceCache

Glide的MemoryCache协议下一共有两个子类,LruResourceCacheMemoryCacheAdapter。 在内存缓存的第二阶段,默认使用的是LruResourceCache,它继承了LruCache作为父类。而实现Lru算法的容器很简单,就是LinkedHashMap。 LinkedHashMap有一个构造函数:

public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

这第三个参数的意思是,按照加入的顺序作为LinkedHashMap的顺序,因此其本身就已经支持Lru的算法核心要素了。那就是常用的value一定会被频繁的remove和put到队尾,那么一旦达到了Lru的maxSize上线,只需要从队头开始删除即可。源码中也是这么做的。 再往后看,于Engine.load()中,发现命中的Resource资源,被加入到了第一步的缓存:ActiveResources中去,引用计数自增1。

private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }

    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {//命中二级内存缓存
      cached.acquire();//资源引用自增1
      activeResources.activate(key, cached);//加入到ActiveResources容器,作为一级缓存。
    }
    return cached;
  }

在第二阶段——LruResourceCache中命中的资源,会首先被从中remove掉。然后资源引用计数值自增,并将资源放入一级内存中。

文件缓存

经过两级内存缓存查找均未命中时,Glide会先判断是否已经有执行中的EngineJob。若有,则返回状态,等待Job回调。否则,实例化新的EngineJob对象,启动执行分级查找方案。EngineJobDeocdeJob两个对象相互依赖。他们之间的关系可以用下图来表示:

文件缓存概述——父类:DataFetchGenerator

对于文件级缓存来说,Glide中提供了三种对象,分别是ResourceCacheGenerator,DataCacheGenerator,SourceGenerator。他们都继承了DataFetchGenerator。 简单的说,DataFetchGenerator就是在对应的缓存级别上,做以下三件事儿:

  1. startNext(), 寻找能够处理当前Model的ModelLoader对象,并返回是否已启动ModelLoader执行FetchData的操作
  2. 若未找到能够执行的ModelLoader,则return false。DecodeJob会执行下一层级的缓存任务。
  3. 若找到了ModelLoader和对应的DataFetcher,则执行loadData(),并回调给DataFetchGenerator。DataFetcherGenerator会回调给DecodeJob,根据数据获取成功与否进行不同的流程。

一级文件缓存——ResourceCacheGenerator

这是基于磁盘缓存的第一级存储,它的startNext()方法如下:

@Override
  public boolean startNext() {
    List<Key> sourceIds = helper.getCacheKeys();
    if (sourceIds.isEmpty()) {
      return false;
    }
    List<Class<?>> resourceClasses = helper.getRegisteredResourceClasses();
    if (resourceClasses.isEmpty()) {
      if (File.class.equals(helper.getTranscodeClass())) {
        return false;
      }
      // TODO(b/73882030): This case gets triggered when it shouldn't. With this assertion it causes
      // all loads to fail. Without this assertion it causes loads to miss the disk cache
      // unnecessarily
      // throw new IllegalStateException(
      //    "Failed to find any load path from " + helper.getModelClass() + " to "
      //        + helper.getTranscodeClass());
    }
    while (modelLoaders == null || !hasNextModelLoader()) {
      resourceClassIndex++;
      if (resourceClassIndex >= resourceClasses.size()) {
        sourceIdIndex++;
        if (sourceIdIndex >= sourceIds.size()) {
          return false;
        }
        resourceClassIndex = 0;
      }

      Key sourceId = sourceIds.get(sourceIdIndex);
      Class<?> resourceClass = resourceClasses.get(resourceClassIndex);
      Transformation<?> transformation = helper.getTransformation(resourceClass);
      // PMD.AvoidInstantiatingObjectsInLoops Each iteration is comparatively expensive anyway,
      // we only run until the first one succeeds, the loop runs for only a limited
      // number of iterations on the order of 10-20 in the worst case.
      currentKey =
          new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoops
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
      cacheFile = helper.getDiskCache().get(currentKey);
      if (cacheFile != null) {
        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;
  }

ResourceCacheGenerator对于modelLoader的搜索流程存在一个双循环,首先找到source列表,然后根据每一个source寻找对应的resource列表。因此这里区分了两个概念:source 和 resource,一个是未经适配的原始资源缓存,另一个是经过宽高等其他配置设置生效后的资源缓存。 startNext()开始首先获取基于model的原始资源Key列表sourceIds。可以说sourceIds中的每一个元素都指向了之前缓存下来的原始数据,并没有经过宽高、大小等缩放处理的数据。然后,遍历sourceIds,通过sourceId,width、height、和options等配置内容生成具备唯一性的ResourceCacheKey,通过modelLoaders找到LoadData,再通过其DataFetcher执行loadData操作。由于loadData不是本文的重点,咱就不展开探索了。

这样下来,ResourceCacheGenerator的主流程就结束了,如果未命中,则经DecodeJob的调度,启动下个级别的Generator:DataCacheGenerator

二级文件缓存——DataCacheGenerator

这是基于磁盘存储的第二级存储方式,其startNext()方法如下:

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

第二级的缓存执行startNext()方法相对来说比较简单,它的目的是找到原始资源,因为在经过第一级缓存搜寻过后没有找到满足当前配置的缓存资源,因此会开始寻找未经修改适配的原始缓存资源。这里只有一个维度的搜索。其方法和原理与ResourceCacheGenerator类似。 如若该层级的缓存依然没有命中,则说明当前需要加载的数据在整个缓存系统中是不存在的,那就需要Glide去亲自从图片原图去请求数据。根据DataCacheGenerator的返回值,经过DecodeJob重新调度,启动SourceGenerator。

源头获取——SourceGenerator

当上述4步缓存策略均未命中时,最终会执行SourceGenerator。与之前的两类DataFetcherGenerator所不一样的是,这里的startNext()不再从cacheKeys缓存中获取数据,也不用再次生成各种Key对象,而是直接从DecodeHelper中获取loadData列表,匹配到DataFetcher执行。

public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      loadData = helper.getLoadData().get(loadDataListIndex++);//直接获取LoadData列表,不再纠结Keys和缓存相关逻辑。
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);//找到能够处理的DataFetcher,执行loadData操作。
      }
    }
    return started;
  }
  
  private boolean hasNextModelLoader() {
    return loadDataListIndex < helper.getLoadData().size();
  }
  

最终我们回顾一下在缓存查找的过程中都发生了什么:

  1. 去活跃缓存查找。
  2. 去内存缓存查找。
  3. 去文件缓存系统查找经过修饰加工的资源。
  4. 去文件缓存系统查找未经修饰加工的原始资源。
  5. 去源头下载,获取新鲜的原始资源。

新获取到的资源如何处理

当新鲜的资源获取到时,

  1. 获取到数据的DataFetcher回调至SourceGenerator中的onDataReady(),告知数据获取成功。
  2. onDataReady()判断DiskCacheStrategy.isDataCacheable。如果是true,通过回调DecodeJob.reschedule()重走一遍流程,使得原图所对应的originKey等属性内容能够整体存入DataCacheGenerator级别的缓存中,并通过其缓存命中的DataFetcher的onDataReady()回调给DecodeJob。
  3. 如果2中isDataCacheable是false,直接走onDataFetcherReady()回调给DecodeJob。
  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    //若是isDataCacheable,则通过reschedule去切换到Glide所在线程中,执行缓存任务。
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;
      // We might be being called back on someone else's thread. Before doing anything, we should
      // reschedule to get back onto Glide's thread.
      cb.reschedule();
    } else {//若不允许DataCache缓存,则直接将原始数据通知回去。
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }
  }
  1. DecodeJob执行decodeFromRetrievedData()对原始Data进行处理获取Resource。并根据DiskCacheStrategy.isResourceCacheable判断是否存储Resource进缓存。
  public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher, DataSource dataSource, Key attemptedKey) {
    this.currentSourceKey = sourceKey;
    this.currentData = data;
    this.currentFetcher = fetcher;
    this.currentDataSource = dataSource;
    this.currentAttemptingKey = attemptedKey;
    if (Thread.currentThread() != currentThread) {//线程判断,若不是Glide所在的当前线程,则通过EngineJob的调度,回到Glide线程,执行Decode操作。
      runReason = RunReason.DECODE_DATA;
      callback.reschedule(this);
    } else {
      GlideTrace.beginSection("DecodeJob.decodeFromRetrievedData");
      try {
        decodeFromRetrievedData();//若已经是Glide线程,则执行decode操作。
      } finally {
        GlideTrace.endSection();
      }
    }
  }
  1. 在DecodeJob中调用notifyComplete(),通过onResourceReady()回调给EngineJob,通知EngineJob在主线程处理Resource。
  private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) {
    if (resource instanceof Initializable) {
      ((Initializable) resource).initialize();
    }

    Resource<R> result = resource;
    LockedResource<R> lockedResource = null;
    if (deferredEncodeManager.hasResourceToEncode()) {
      lockedResource = LockedResource.obtain(resource);
      result = lockedResource;
    }

    notifyComplete(result, dataSource);//先发出通知

    stage = Stage.ENCODE;
    try {
      if (deferredEncodeManager.hasResourceToEncode()) {
        deferredEncodeManager.encode(diskCacheProvider, options);//再进行一级文件缓存
      }
    } finally {
      if (lockedResource != null) {
        lockedResource.unlock();
      }
    }
    // Call onEncodeComplete outside the finally block so that it's not called if the encode process
    // throws.
    onEncodeComplete();
  }
  
  private void notifyComplete(Resource<R> resource, DataSource dataSource) {
    setNotifiedOrThrow();
    callback.onResourceReady(resource, dataSource);
  }
  1. EngineJob中收到回调,执行handleResultOnMainThread()handlResultOnMainThread()中组装EngineResource,并加入到ActiveResource缓存中,resource引用计数自增。
  public void onResourceReady(Resource<R> resource, DataSource dataSource) {//收到回调
    this.resource = resource;
    this.dataSource = dataSource;
    MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();//主线程传递消息
  }
  
  public boolean handleMessage(Message message) {//主线程运行,处理携带资源的EngineJob。
      EngineJob<?> job = (EngineJob<?>) message.obj;
      switch (message.what) {
        case MSG_COMPLETE:
          job.handleResultOnMainThread();
          break;
        case MSG_EXCEPTION:
          job.handleExceptionOnMainThread();
          break;
        case MSG_CANCELLED:
          job.handleCancelledOnMainThread();
          break;
        default:
          throw new IllegalStateException("Unrecognized message: " + message.what);
      }
      return true;
    }
    
 void handleResultOnMainThread() {
    stateVerifier.throwIfRecycled();
    if (isCancelled) {
      resource.recycle();
      release(false /*isRemovedFromQueue*/);
      return;
    } else if (cbs.isEmpty()) {
      throw new IllegalStateException("Received a resource without any callbacks to notify");
    } else if (hasResource) {
      throw new IllegalStateException("Already have resource");
    }
    engineResource = engineResourceFactory.build(resource, isCacheable);//构建engineResource
    hasResource = true;

    // Hold on to resource for duration of request so we don't recycle it in the middle of
    // notifying if it synchronously released by one of the callbacks.
    engineResource.acquire();//engineResource引用计数自增
    listener.onEngineJobComplete(this, key, engineResource);//通知Engine,EngineJob执行完毕,

    //noinspection ForLoopReplaceableByForEach to improve perf
    for (int i = 0, size = cbs.size(); i < size; i++) {
      ResourceCallback cb = cbs.get(i);
      if (!isInIgnoredCallbacks(cb)) {
        engineResource.acquire();
        cb.onResourceReady(engineResource, dataSource);//通知Request,数据获取Ready。
      }
    }
    // Our request is complete, so we can release the resource.
    engineResource.release();

    release(false /*isRemovedFromQueue*/);
  }
  1. Engine收到onEngineJobComplete的回调,根据缓存策略,将engineResource存入活跃缓存,并移除当前Job。自此,EngineJob完成使命,寿终正寝。
  2. 通知外部Request,回调其onResourceReady(),将Resource和Target关联起来。

总结

经过上面的流程分析,我们再回到开始的两个问题:

  1. Glide到底有几级缓存?这些缓存都是什么?

答: 这个问题是多方面的。从执行步骤上来说,算上SourceGenerator,一共有五级。分别是ActiveResource,LruResourceCache,ResourceCacheGenerator,DataCacheGenerator,SourceGenerator。如果从存储介质的角度来讲,就是三级:MEMORY_CACHE(内存缓存), DISK_CACHE(文件缓存),和原始数据(可能是本地或远端)。

  1. 当Glide获取资源后,处理资源的流程是什么样的?

根据最后一节的分析,基本是在DiskCacheStrategy的约束下,从SourceGenerator环节开始,依次回调,并进行对应层面的存储。最终设置给Target。