Glide 缓存设计分析

1,513 阅读6分钟

官方必读宝典:文档

01 - Glide 整体分析

Glide 中的缓存类型

  • 活动资源 (Active Resources) - 现在是否有另一个 View 正在展示这张图片?
  • 内存缓存 (Memory cache) - 该图片是否最近被加载过并仍存在于内存中?
  • 资源类型(Resource) - 该图片是否之前曾被解码、转换并写入过磁盘缓存?
  • 数据来源 (Data) - 构建这个图片的资源是否之前曾被写入过文件缓存?

从内存加载

活动资源(ActiveResources)

活动资源表示即将加载的图片正在被别的 ImageView 展示。

Glide 中使用弱引用收集了正在展示的图片数据,如果新加载的数据的 key 和正在被 ImageView 展示的数据 key 一致,就会获取活动资源中的数据被使用。

// Engine.java
public <R> LoadStatus load(
···
···
···) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;
    // 这个 key 用于缓存
    EngineKey key =
        keyFactory.buildKey(
            model,
            signature,
            width,
            height,
            transformations,
            resourceClass,
            transcodeClass,
            options);

    EngineResource<?> memoryResource;
    synchronized (this) {
      // 从内存中加载数据
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
      // 内存中没有,开始一个新的数据加载
      if (memoryResource == null) {
        return waitForExistingOrStartNewJob(
 				···
 				···
 				···
 				;
      }
    }
    // 如果成功的从内存中加载了数据
    cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
    return null;
  }
  
  
  // 从内存中加载数据
    private EngineResource<?> loadFromMemory(
      EngineKey key, boolean isMemoryCacheable, long startTime) {
    if (!isMemoryCacheable) {
      return null;
    }
    // 从活动的资源中加载,意思是此资源在其他处已经被加载或者使用
    // 这里使用内部的是弱引用
    EngineResource<?> active = loadFromActiveResources(key);
    if (active != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return active;
    }
    // 从内存缓存中加载
    EngineResource<?> cached = loadFromCache(key);
    if (cached != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return cached;
    }

    return null;
  }
  
    private EngineResource<?> loadFromActiveResources(Key key) {
    // 从正在显示的资源中获取
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {
      // 使用一次,进行计数
      active.acquire();
    }
    return active;
  }

从内存中加载(近期被使用过的资源)

  private EngineResource<?> loadFromCache(Key key) {
    // 从内存中获取,原理使用的是 Lru 缓存策略
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      // 使用计数
      cached.acquire();
      // 将其添加至正在显示的资源中
      activeResources.activate(key, cached);
    }
    return cached;
  }

  private EngineResource<?> getEngineResourceFromCache(Key key) {
    // 从内存中取出资源并从内存中删除,cache 默认使用 LruResourceCache.java
    Resource<?> cached = cache.remove(key);

    final EngineResource<?> result;
    if (cached == null) {
      result = null;
    } else if (cached instanceof EngineResource) {
      // Save an object allocation if we've cached an EngineResource (the typical case).
      result = (EngineResource<?>) cached;
    } else {
      // 如果获取到的资源不是 EngineResource 则将其转
      result =
          new EngineResource<>(
              cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
    }
    return result;
  }

从磁盘加载

Glide 从磁盘加载时考虑到了将图片从二进制数据解码至对应的图片格式的资源消耗,因此在将数据缓存至磁盘的时候可以缓存两种数据,一种是为转码的二进制原始数据,二种是经过转码并附带转换配置选项的数据,可以经过缓存配置选项配置磁盘的缓存策略。同一数据不同状态的数据,通过不同的 key 缓存至磁盘。

加载转换过的数据

  // ResourceCacheGenerator.java

	public boolean startNext() {
    // 获取已缓存数据的所有数据的 key
    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;
      }
      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;
  }

加载原始缓存数据

  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 不带宽度高度、转换选项
      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;
  }

缓存键的设计

如开头所述,Glide 支持多种缓存并且同一路径图片可能以不同的形态缓存多次,简单是使用图片路径作为缓存键是不可行地,因此 Glide 设计了缓存键来支持。

以下是几个常用的 key

  • EngineKey 内存缓存使用
  • DataCacheKey 未解码的资源数据使用
  • ResourceCacheKey 解码过的资源数据使用

缓存的配置设计

Glide 的缓存配置有两处,全局配置和当个配置。同时内存缓存和磁盘缓存是默认开启的。

内存缓存

	// 默认使用内存缓存
	private boolean isCacheable = true;

	// 设置是否使用内存缓存
  public T skipMemoryCache(boolean skip) {
    if (isAutoCloneEnabled) {
      return clone().skipMemoryCache(true);
    }
    // 修改内存缓存选项
    this.isCacheable = !skip;
    fields |= IS_CACHEABLE;

    return selfOrThrowIfLocked();
  }

  private EngineResource<?> loadFromMemory(
      EngineKey key, boolean isMemoryCacheable, long startTime) {
    // 判断内存缓存开关
    if (!isMemoryCacheable) {
      return null;
    }
		····
    ····
    return null;
  }

磁盘缓存

磁盘缓存会在初始化图片加载模型与从数据源处加载图片会进行判断使用。主要依赖 DiskCacheStrategy 设置磁盘缓存类型,一共有五种类型,ALLNONEDATARESOURCEAUTOMATIC ,其中 AUTOMATIC为默认。这五种类型又是一 DiskCacheStrategy 四个方法返回不同的返回值来确定的。

	// 开始加载图片时
	private Stage getNextStage(Stage current) {
    //PS: 这里就是我们在使用时设置缓存策略的时候最终的值
    switch (current) {
      case INITIALIZE:
        // 通过对缓存模型的判断,判断是否从缓存的数据中解码,缓存的就直接返回Stage.RESOURCE_CACHE
        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:
        // Skip loading from source if the user opted to only retrieve the resource from cache.
        // 如果设置了仅仅从缓存中加载数据,那么跳过缓存从源数据中加载
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
  }
  /**
   * 数据加载成功后调用
   * @param data 加载成功的数据
   */
  @Override
  public void onDataReady(Object data) {
    // 获取磁盘缓存策略
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    // 判断是否使用缓存策略,最终调用的两个回调都在 DecodeJob.java 中
    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 回调是在构建此加载模型的时候传入的,因此后续的看 DecodeJob.java 中的两个方法。
      cb.reschedule();
    } else {
      // 不使用缓存
      cb.onDataFetcherReady(
          loadData.sourceKey,
          data,
          loadData.fetcher,
          loadData.fetcher.getDataSource(),
          originalKey);
    }
  }

两种缓存空间大小设计

内存缓存和磁盘缓存都是基于 LRC设计,很容易的通过配置设置缓存控件大小。

缓存的刷新设计

缓存刷新是个难题,因为最好使得缓存失效的办法就是改变资源路径,但是在某些业务中资源路径是不可能更改的,因此 Glide 设计了sign来完成对缓存键的更改。机制没有了解清楚。。。待解决。