[Glide系列第3篇]Glide源码分析之缓存处理

Android缓存机制:如果没有缓存,在大量的网络请求从远程获取图片时会造成网络流量的浪费,加载速度较慢,用户体验不好

关于学习Glide缓存原理前十分建议你先了解图片加载的流程,在这基础上再进行学习会更加上手。然后可以看思维导图,从宏观角度理解Glide加载。本文的源码基于V4版本

Glide系列文章

Glide源码分析流程思维导图

【两篇就懂系列】Glide源码分析之加载图片流程(1/2)

【两篇就懂系列】Glide源码分析之加载图片流程(2/2)

Glide图片加载库从v3迁移到v4的改变和使用


【Glide的缓存】

在缓存这一功能上,Glide将它分成了两个模块,一个是内存缓存,一个是硬盘缓存。同时内存缓存又分为两级,一级是LruCache缓存,一级是弱引用缓存。
  • 内存缓存的作用:防止应用重复将图片数据读取到内存当中。
    • LruCache缓存:不在使用中的图片使用LruCache来进行缓存。
    • 弱引用缓存:把正在使用中的图片使用弱引用来进行缓存。
      【这样的目的保护正在使用的资源不会被LruCache算法回收。】
  • 硬盘缓存的作用:防止应用重复从网络或其他地方重复下载和读取数据。

默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存:

  • 内存缓存 (Memory cache) - 该图片是否最近被加载过并仍存在于内存中?即LruCache缓存。
  • 活动资源 (Active Resources) - 现在是否有另一个 View 正在展示这张图片?也就是弱引用缓存。
  • 资源类型(Resource) - 该图片是否之前曾被解码、转换并写入过磁盘缓存?
  • 数据来源 (Data) - 构建这个图片的资源是否之前曾被写入过文件缓存?
    前两步检查图片是否在内存中,如果是则直接返回图片。后两步则检查图片是否在磁盘上,以便快速但异步地返回图片。
    如果四个步骤都未能找到图片,则Glide会返回到原始资源以取回数据(原始文件,Uri, Url等)

结合源码去分析Glide缓存

首先在[图片加载源码分析一]的文章中,我们在通过单例获取Glide的实例时,调用过checkAndInitializeGlide(context)这个方法,在具体的方法里有一段代码是通过GlideBuilder.build初始化一些对象,如下

GlideBuilder类
public Glide build(Context context) {
    if (sourceExecutor == null) {
      sourceExecutor = GlideExecutor.newSourceExecutor();
    }

    if (diskCacheExecutor == null) {
      diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();
    }

    if (memorySizeCalculator == null) {
      memorySizeCalculator = new MemorySizeCalculator.Builder(context).build();
    }

    if (connectivityMonitorFactory == null) {
      connectivityMonitorFactory = new DefaultConnectivityMonitorFactory();
    }

    if (bitmapPool == null) {
      int size = memorySizeCalculator.getBitmapPoolSize();
      if (size > 0) {
        bitmapPool = new LruBitmapPool(size);
      } else {
        bitmapPool = new BitmapPoolAdapter();
      }
    }

    if (arrayPool == null) {
      arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes());
    }
  //1
    if (memoryCache == null) {
      memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
    }
  //2
    if (diskCacheFactory == null) {
      diskCacheFactory = new InternalCacheDiskCacheFactory(context);
    }

    if (engine == null) {
      engine =
          new Engine(
              memoryCache,
              diskCacheFactory,
              diskCacheExecutor,
              sourceExecutor,
              GlideExecutor.newUnlimitedSourceExecutor(),
              GlideExecutor.newAnimationExecutor());
    }

    RequestManagerRetriever requestManagerRetriever = new RequestManagerRetriever(
        requestManagerFactory);

    return new Glide(
        context,
        engine,
        memoryCache,
        bitmapPool,
        arrayPool,
        requestManagerRetriever,
        connectivityMonitorFactory,
        logLevel,
        defaultRequestOptions.lock(),
        defaultTransitionOptions);
  }

在我代码中标记1和2处,1处new出了一个LruResourceCache,并把它赋值到了memoryCache这个对象上面。你没有猜错,这个就是Glide实现内存缓存所使用的LruCache对象了。2处InternalCacheDiskCacheFactory是磁盘缓存(内部存储)所使用的工厂对象。同时在其中初始化了磁盘缓存的大小和文件的路径。

创建好了这些对象说明我们已经把准备工作做好了。

【内存缓存】

接口为MemoryCache,Glide使用LruResourceCache作为默认的内存缓存,该类是接口MemoryCache的一个缺省实现(接口的另一个实现类为MemoryCacheAdapter)。使用固定大小的内存和 LRU 算法。LruResourceCache的大小由 Glide 的MemorySizeCalculator类来决定,这个类主要关注设备的内存类型,设备 RAM 大小,以及屏幕分辨率。

【内存缓存的读取】

我们先分析Glide从哪里读取内存缓存,以及内存缓存的原理。

1. Lurcache算法
对于大多内存缓存的实现,我们通常会知道这样一个算法,LruCache算法(Least Recently Used),也叫近期最少使用算法。它的主要算法原理就是把最近使用的对象用强引用存储在LinkedHashMap(双向循环列表)中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。淘汰最长时间未使用的对象
上面我们提到的LruResourceCache就是Glide实现内存缓存所使用的LruCache对象了,而这个类继承LruCache。也是最近最少使用算法的具体实现。
public class LruCache<T, Y> {
  private final LinkedHashMap<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
  private final int initialMaxSize;
  private int maxSize;
  private int currentSize = 0;

  /**
   * Constructor for LruCache.
   *
   * @param size The maximum size of the cache, the units must match the units used in {@link
   *             #getSize(Object)}.
   */
  public LruCache(int size) {
    this.initialMaxSize = size;
    this.maxSize = size;
  }

  /**
   * Sets a size multiplier that will be applied to the size provided in the constructor to put the
   * new size of the cache. If the new size is less than the current size, entries will be evicted
   * until the current size is less than or equal to the new size.
   *
   * @param multiplier The multiplier to apply.
   */
  public synchronized void setSizeMultiplier(float multiplier) {
    if (multiplier < 0) {
      throw new IllegalArgumentException("Multiplier must be >= 0");
    }
    maxSize = Math.round(initialMaxSize * multiplier);
    evict();
  }

  /**
   * Returns the size of a given item, defaulting to one. The units must match those used in the
   * size passed in to the constructor. Subclasses can override this method to return sizes in
   * various units, usually bytes.
   *
   * @param item The item to get the size of.
   */
  protected int getSize(Y item) {
    return 1;
  }

  /**
   * Returns the number of entries stored in cache.
   */
  protected synchronized int getCount() {
    return cache.size();
  }

  /**
   * A callback called whenever an item is evicted from the cache. Subclasses can override.
   *
   * @param key  The key of the evicted item.
   * @param item The evicted item.
   */
  protected void onItemEvicted(T key, Y item) {
    // optional override
  }

  /**
   * Returns the current maximum size of the cache in bytes.
   */
  public synchronized int getMaxSize() {
    return maxSize;
  }

  /**
   * Returns the sum of the sizes of all items in the cache.
   */
  public synchronized int getCurrentSize() {
    return currentSize;
  }

  /**
   * Returns true if there is a value for the given key in the cache.
   *
   * @param key The key to check.
   */

  public synchronized boolean contains(T key) {
    return cache.containsKey(key);
  }

  /**
   * Returns the item in the cache for the given key or null if no such item exists.
   *
   * @param key The key to check.
   */
  @Nullable
  public synchronized Y get(T key) {
    return cache.get(key);
  }

  /**
   * Adds the given item to the cache with the given key and returns any previous entry for the
   * given key that may have already been in the cache.
   *
   * <p> If the size of the item is larger than the total cache size, the item will not be added to
   * the cache and instead {@link #onItemEvicted(Object, Object)} will be called synchronously with
   * the given key and item. </p>
   *
   * @param key  The key to add the item at.
   * @param item The item to add.
   */
  public synchronized Y put(T key, Y item) {
    final int itemSize = getSize(item);
    if (itemSize >= maxSize) {
      onItemEvicted(key, item);
      return null;
    }

    final Y result = cache.put(key, item);
    if (item != null) {
      currentSize += getSize(item);
    }
    if (result != null) {
      // TODO: should we call onItemEvicted here?
      currentSize -= getSize(result);
    }
    evict();

    return result;
  }

  /**
   * Removes the item at the given key and returns the removed item if present, and null otherwise.
   *
   * @param key The key to remove the item at.
   */
  @Nullable
  public synchronized Y remove(T key) {
    final Y value = cache.remove(key);
    if (value != null) {
      currentSize -= getSize(value);
    }
    return value;
  }

  /**
   * Clears all items in the cache.
   */
  public void clearMemory() {
    trimToSize(0);
  }

  /**
   * Removes the least recently used items from the cache until the current size is less than the
   * given size.
   *
   * @param size The size the cache should be less than.
   */
  protected synchronized void trimToSize(int size) {
    Map.Entry<T, Y> last;
    while (currentSize > size) {
      last = cache.entrySet().iterator().next();
      final Y toRemove = last.getValue();
      currentSize -= getSize(toRemove);
      final T key = last.getKey();
      cache.remove(key);
      onItemEvicted(key, toRemove);
    }
  }

  private void evict() {
    trimToSize(maxSize);
  }
}
2. Glide内存缓存的实现自然也是使用的LruCache算法。不过除了LruCache算法之外,Glide还结合了一种弱引用的机制,共同完成了内存缓存功能。这样做的目的是把正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用LruCache来进行缓存的功能。分工合作,保护正在使用的资源不会被LruCache算法回收掉。(划重点)
3. Glide默认情况下,Glide自动就是开启内存缓存的。也就是说,当我们使用Glide加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用Glide再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了,这样无疑就可以大幅度提升图片的加载效率。比方我们在使用RecyclerView、listview、viewpager这种控件时,反复上下滑动,当移出屏幕的项被回收再次移入屏幕展示时,那么只要是Glide加载过的图片都可以直接从内存当中迅速读取并展示出来。
4. 看过了之前图片加载的两篇文章,我们在第三步into时,onSizeReady准备图片加载时,会调用Engine.load这个比较重要的方法,在上一篇文章分析时,我们忽略了对缓存的处理,而是直接分析没有缓存的加载过程。而这篇文章我们返回看缓存处理。
重新放出Engine.load方法。
Engine类
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 = LogTime.getLogTime();
  //a
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);
  //b
    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return null;
    }
  //c
    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return null;
    }
  //d
    EngineJob<?> current = jobs.get(key);
    if (current != null) {
      current.addCallback(cb);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Added to existing load", startTime, key);
      }
      return new LoadStatus(cb, current);
    }
  //e
    EngineJob<R> engineJob = engineJobFactory.build(key, isMemoryCacheable,
        useUnlimitedSourceExecutorPool, useAnimationPool);
    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 (Log.isLoggable(TAG, Log.VERBOSE)) {
      logWithTimeAndKey("Started new load", startTime, key);
    }
    return new LoadStatus(cb, engineJob);
  }
  • //a处:首先通过KeyFactory的buildKey方法创建了一个EngineKey对象(缓存键),这个对象就是我们说的缓存key,加载资源的唯一标识。可以看到决定缓存Key的条件非常多,即使你用override()方法改变了一下图片的width或者height,也会生成一个完全不同的缓存Key。
  • //b处:通过loadFromCache方法,通过key查找缓存资源,此时的缓存为内存缓存,如果获取的到就直接调用cb.onResourceReady()方法进行回调。
  • //c处:如果内存缓存没有找到对应key的资源,则调用loadFromActiveResources方法,还是通过key获取缓存资源,而此时的缓存也是内存缓存。获取到的话也直接进行回调。
也就是说,Glide的图片加载过程中会调用两个方法来获取内存缓存,loadFromCache()和loadFromActiveResources()。这两个方法中前者使用的就是LruCache算法,后者使用的就是弱引用。
  • //d处:如果以上都没有找到,那是否可能该缓存任务正在处理,还没有完成缓存,所以根据key判断缓存的job中是否有current,如果有,就不用新创建任务了,而是给其添加回调,等待完成后获取。
  • //e处:如果以上条件都不满足,我们就需要创建新的加载任务。并把当前任务存放在jobs这个map中。同时要开启线程来加载新的图片了。
5.看一下loadFromCache()和loadFromActiveResources()这两个方法的源码:
//Engine类
  private final MemoryCache cache;
  private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
...省略
//内存缓存 (Memory cache) - 该图片是否最近被加载过并仍存在于内存中?
 private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
  //a
    if (!isMemoryCacheable) {
      return null;
    }

    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
  //c
      cached.acquire();
  //d
      activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
    }
    return cached;
  }

  @SuppressWarnings("unchecked")
  private EngineResource<?> getEngineResourceFromCache(Key key) {
  //b
    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 {
      result = new EngineResource<>(cached, true /*isMemoryCacheable*/);
    }
    return result;
  }
//活动资源 (Active Resources) - 现在是否有另一个 View 正在展示这张图片?
 //e
  private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }

    EngineResource<?> active = null;
    WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
    if (activeRef != null) {
      active = activeRef.get();//得到引用的资源
  //再次判断是为了防止引用被清空,或gc回收
      if (active != null) {
        active.acquire();
      } else {
        activeResources.remove(key);
      }
    }

    return active;
  }
  • //a处:首先关于传入参数isMemoryCacheable,代表内存缓存是否被开启,Glide默认为开启,true。但如果想要禁用的话呢?通过向skipMemoryCache()传入true,此时isMemoryCacheable将为false,返回值也为null。Glide-v3到v4写法的变化
GlideApp.with(fragment)
  .load(url)
  .skipMemoryCache(true)
  .into(view);
  • //b处:接着调用了getEngineResourceFromCache(key)方法来获取缓存。在这个方法中,会使用缓存Key来从cache当中取值,而这里的cache对象就是在构建Glide对象时创建的LruResourceCache,那么说明这里其实使用的就是LruCache算法了。当我们从LruResourceCache中获取到缓存图片之后会将它从缓存中移除。cache.remove(key)。这个语句既返回了对应key的value值,也将对应的key从cache中移除。
  • //c处:如果cached不为null,首先调用cached.acquire();EngineResource用一个acquired变量来记录图片被引用的次数,调用acquire()方法会让变量acquire加1,调用release()方法会让变量减1。(当acquired变量大于0的时候,说明图片正在使用中,也就应该放到activeResources弱引用缓存当中,如果acquired变量等于0了,说明图片已经不在使用中了。释放资源,从activeResources弱引用缓存中移除,并put到LruResourceCache当中)
  • //d处:然后将这个缓存图片存储到activeResources当中。activeResources就是一个弱引用的HashMap,用来缓存正在使用中的图片,我们可以看到,loadFromActiveResources()方法就是从activeResources这个HashMap当中取值的。使用activeResources来缓存正在使用中的图片,可以保护这些图片不会被LruCache算法回收掉。
  • //e处:如果从内存中没有找到资源,那有一种可能为该资源已被LruCache算法移除,但是它正在被另一个view展示,所以此时还是有此资源的缓存。所以查找,存在,引用值加1,不存在,则把key从activeResources弱引用缓存中移除。

【内存缓存的写入】

1.当没有读取到缓存时,我们肯定要正常开启线程去下载资源了,具体流程可以看之前的文章,那么从服务端得到资源后是何时以及如何写入到缓存中呢?下面来具体分析:

上一篇文章讲解过,当从服务端得到stream然后做处理得到的最终图片资源通过层层回调返回等最终交给EngineJob的onResourceReady的方法处理。而在这个方法中通过Handler发送一条消息将执行逻辑切回到主线程当中,从而执行handleResultOnMainThread()方法。

EngineJob类
...
  private final EngineJobListener listener;
... 
 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);
    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();
    listener.onEngineJobComplete(key, engineResource);

    for (ResourceCallback cb : cbs) {
      if (!isInIgnoredCallbacks(cb)) {
        engineResource.acquire();
        cb.onResourceReady(engineResource, dataSource);
      }
    }
    // Our request is complete, so we can release the resource.
    engineResource.release();

    release(false /*isRemovedFromQueue*/);
  }

在这个方法里,通过EngineResourceFactory构建出了一个包含图片资源的EngineResource对象,然后将这个对象回调到Engine的onEngineJobComplete()方法当中.

Engine类
public class Engine implements EngineJobListener,
    MemoryCache.ResourceRemovedListener,
    EngineResource.ResourceListener {
...
  @SuppressWarnings("unchecked")
  @Override
  public void onEngineJobComplete(Key key, EngineResource<?> resource) {
    Util.assertMainThread();
    // A null resource indicates that the load failed, usually due to an exception.
    if (resource != null) {
      resource.setResourceListener(key, this);

      if (resource.isCacheable()) {
        activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
      }
    }
    // TODO: should this check that the engine job is still current?
    jobs.remove(key);
  }
}
重点:先将resource添加监听,然后回调过来的EngineResource被put到了activeResources当中,在这里写入到了内存缓存的弱引用缓存。写入到弱引用缓存的原因是这个资源是属于正在被加载展示的资源,也就是正在被使用的资源。
那什么时候要写入到内存缓存的LruCache中呢?我们说过要将不在使用中的图片使用LruCache来进行缓存,那怎么判断是否在使用中?那就是前面讲到的EngineResource中的一个引用机制,通过acquired的值来判断。(当acquired变量大于0的时候,说明图片正在使用中,也就应该放到activeResources弱引用缓存当中,如果acquired变量等于0了,说明图片已经不在使用中了,此时放到LruCache来进行缓存。)acquired的增加和减少通过EngineResource的acquire()和release()方法。
EngineResource类
class EngineResource<Z> implements Resource<Z> {
  private int acquired;
  private ResourceListener listener;
  ...
  void acquire() {
    if (isRecycled) {
      throw new IllegalStateException("Cannot acquire a recycled resource");
    }
    if (!Looper.getMainLooper().equals(Looper.myLooper())) {
      throw new IllegalThreadStateException("Must call acquire on the main thread");
    }
    ++acquired;
  }
  void release() {
    if (acquired <= 0) {
      throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
    }
    if (!Looper.getMainLooper().equals(Looper.myLooper())) {
      throw new IllegalThreadStateException("Must call release on the main thread");
    }
    if (--acquired == 0) {
      listener.onResourceReleased(key, this);
    }
  }
}
在release方法可以看到,当acquired=0时,调用engine的onResourceReleased();
Engine类
  @Override
  public void onResourceReleased(Key cacheKey, EngineResource resource) {
    Util.assertMainThread();
    activeResources.remove(cacheKey);
    if (resource.isCacheable()) {
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource);
    }
  }
当不在使用中时,此时就可以从 activeResources移除,同时就可以添加到Lrucache中了。此处为写入内存缓存的LruCache地方。
截止到此 内存缓存分析完毕。

【磁盘缓存】

接口为DiskCache,Glide 使用DiskLruCacheWrapper作为默认的磁盘缓存,该类是接口MemoryCache的实现类(该接口的另一个实现类为DiskCacheAdapter)。 DiskLruCacheWrapper是一个使用 LRU 算法的固定大小的磁盘缓存。默认磁盘大小为250MB,位置是在应用的缓存文件夹 下中的一个 特定目录
Google也曾提供了一个现成的工具类,DiskLruCache。郭霖大神这篇文章对这个DiskLruCache工具进行了比较全面的分析,感兴趣的朋友可以参考一下 Android DiskLruCache完全解析,硬盘缓存的最佳方案
默认情况下我们进行初始化glide时是磁盘内部存储new InternalCacheDiskCacheFactory(context),假如应用程序展示的媒体内容是公开的(从无授权机制的网站上加载,或搜索引擎等),那么应用可以将这个缓存位置改到外部存储:在自定义moudle的配置GlideBuilder.setDiskCache(new ExternalDiskCacheFactory(context));
无论使用内部或外部磁盘缓存,应用程序都可以改变磁盘缓存的大小和改变缓存文件夹在外存或内存上的名字:
官网示例:
@GlideModule
public class YourAppGlideModule extends AppGlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    int diskCacheSizeBytes = 1024  1024  100;  100 MB
    builder.setDiskCache(
        new InternalDiskCacheFactory(context, cacheFolderName, diskCacheSizeBytes));
  }
}
上面我们提到过内存缓存可以禁止,同样磁盘缓存也可以。
GlideApp.with(fragment)
  .load(url)
  .diskCacheStrategy(DiskCacheStrategy.NONE)
  .into(view);

【磁盘缓存策略】

看到diskCacheStrategy()方法,我们就必须要提一下磁盘缓存策略:DiskCacheStrategy可被diskCacheStrategy方法应用到每一个单独的请求。 目前支持的策略如下
  • DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。对于远程图片,缓存DATA和RESOURCE。对于本地图片,只缓存RESOURCE。
  • DiskCacheStrategy.AUTOMATIC :它会尝试对本地和远程图片使用最佳的策略。当你加载远程数据(比如,从URL下载)时,AUTOMATIC 策略仅会存储未被你的加载过程修改过(比如,变换,裁剪–译者注)的原始数据(DATA),因为下载远程数据相比调整磁盘上已经存在的数据要昂贵得多。对于本地数据,AUTOMATIC 策略则会仅存储变换过的缩略图(RESOURCE),因为即使你需要再次生成另一个尺寸或类型的图片,取回原始数据也很容易。
  • DiskCacheStrategy.DATA:表示只缓存未被处理的文件。我的理解就是我们获得的stream。它是不会被展示出来的,需要经过装载decode,对图片进行压缩和转换,等等操作,得到最终的图片才能被展示。
  • DiskCacheStrategy.NONE: 表示不缓存任何内容。
  • DiskCacheStrategy.RESOURCE:表示只缓存转换过后的图片。(也就是经过decode,转化裁剪的图片)
默认的策略为DiskCacheStrategy.AUTOMATIC,改变策略也很简单, xxx.diskCacheStrategy(DiskCacheStrategy.ALL);

【磁盘缓存的读取】

上面讲过内存缓存的读取,那磁盘缓存是在哪里读取的呢?和内存缓存一样,我们触发图片的加载是在Engine的load方法中,当我们从内存缓存以及当前任务中都没有找到资源时,我们要开启线程去下载,engineJob.start(decodeJob);上一篇文章因为忽略缓存,考虑加载,所以当时忽略缓存操作,而这次我们带着缓存一起看。
EngineJob类
public void start(DecodeJob<R> decodeJob) {
    this.decodeJob = decodeJob;
    GlideExecutor executor = decodeJob.willDecodeFromCache()
        ? diskCacheExecutor
        : getActiveSourceExecutor();
    executor.execute(decodeJob);
  }
DecodeJob类
  /**
   * Returns true if this job will attempt to decode a resource from the disk cache, and false if it
   * will always decode from source.
   */
  boolean willDecodeFromCache() {
    Stage firstStage = getNextStage(Stage.INITIALIZE);
    return firstStage == Stage.RESOURCE_CACHE || firstStage == Stage.DATA_CACHE;
  }

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

  • willDecodeFromCache()方法通过调用getNextStage,传入初始化标识INITIALIZE,得到当前阶段标识,diskCacheStrategy.decodeCachedResource()返回一个boolean标识,如果我们指定磁盘缓存策略为DiskCacheStrategy.ALL或DiskCacheStrategy.RESOURCE或由DiskCacheStrategy.AUTOMATIC对远程图片使用磁盘缓存时,此时返回true,标识。返回Stage.RESOURCE_CACHE。如果为false,递归调用,判断是否为diskCacheStrategy.decodeCachedData(),也就是指定磁盘缓存策略为DiskCacheStrategy.ALL或DiskCacheStrategy.DATA或由DiskCacheStrategy.AUTOMATIC对本地图片使用磁盘缓存时,此时返回true。否则返回false,递归调用,判断onlyRetrieveFromCache的boolean值,这个值是初始化DecodeJob中传进来的,它代表是否仅从缓存加载图片,通过onlyRetrieveFromCache(true)制定,默认为false,如果为true,它意味着要从内存或磁盘读取,如果内存或磁盘不存在该资源,则加载直接失败。一般情况下我们不会制定,为false,也就是会返回 Stage.SOURCE。代表不使用磁盘缓存,也就是之前文章分析的,直接从服务器下载。关于onlyRetrieveFromCache,再多说两句:
某些情形下,你可能希望只要图片不在缓存中则加载直接失败(比如省流量模式?–译者注)。如果要完成这个目标,你可以在单个请求的基础上使用 
GlideApp.with(fragment)
  .load(url)
  .onlyRetrieveFromCache(true)
  .into(imageView);
  • 所以,如果getNextStage方法返回的标识为Stage.RESOURCE_CACHE或Stage.DATA_CACHE就代表我们没有禁止磁盘缓存,那么willDecodeFromCache()将返回true。此时executor=diskCacheExecutor,返回false,executor=getActiveSourceExecutor();而这些executor在glide初始化的GlideBuilder.build方法里已经被实例了。
  • 然后执行executor.execute(decodeJob);接着会command.run();开启线程任务,也就是执行DecodeJob的run方法,run方法里调用runWrapped方法,runReason默认为INITIALIZE
DecodeJob类
private void runWrapped() {
     switch (runReason) {
      case INITIALIZE:
        // 初始化 获取下一个阶段状态
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        // 运行
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }
  
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 Stage.SOURCE;
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }

// 根据Stage找到数据抓取生成器。
private DataFetcherGenerator getNextGenerator() {
    switch (stage) {
      case RESOURCE_CACHE:
       // 产生含有降低采样/转换资源数据缓存文件的DataFetcher。
        return new ResourceCacheGenerator(decodeHelper, this);
      case DATA_CACHE:
       // 产生包含原始未修改的源数据缓存文件的DataFetcher。
        return new DataCacheGenerator(decodeHelper, this);
      case SOURCE:
      // 生成使用注册的ModelLoader和加载时提供的Model获取源数据规定的DataFetcher。
      // 根据不同的磁盘缓存策略,源数据可首先被写入到磁盘,然后从缓存文件中加载,而不是直接返回。
        return new SourceGenerator(decodeHelper, this);
      case FINISHED:
        return null;
      default:
        throw new IllegalStateException("Unrecognized stage: " + stage);
    }
  }
 private void runGenerators() {
    currentThread = Thread.currentThread();
    startFetchTime = LogTime.getLogTime();
    boolean isStarted = false;
    while (!isCancelled && currentGenerator != null
        && !(isStarted = currentGenerator.startNext())) {
      stage = getNextStage(stage);
      currentGenerator = getNextGenerator();

      if (stage == Stage.SOURCE) {
        reschedule();
        return;
      }
    }
    // We've run out of stages and generators, give up.
    if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
      notifyFailed();
    }

    // Otherwise a generator started a new load and we expect to be called back in
    // onDataFetcherReady.
  }
这里我们选择ResourceCacheGenerator或DataCacheGenerator都好,我们就以ResourceGenerator为示例,在runGenerators()方法里,还是看currentGenerator.startNext()。
ResourceCacheGenerator类
@Override
  public boolean startNext() {
    List<Key> sourceIds = helper.getCacheKeys();
    if (sourceIds.isEmpty()) {
      return false;
    }
    List<Class<?>> resourceClasses = helper.getRegisteredResourceClasses();
    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);

      currentKey = new ResourceCacheKey(sourceId, helper.getSignature(), helper.getWidth(),
          helper.getHeight(), transformation, resourceClass, helper.getOptions());
      cacheFile = helper.getDiskCache().get(currentKey);
      if (cacheFile != null) {
        this.sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }

    loadData = null;
    boolean started = false;
// 查找ModelLoader 
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData =
          modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(),
              helper.getOptions());
  // 通过FileLoader继续加载数据
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }

    return started;
  }
FileLoad类
public void loadData(Priority priority, DataCallback<? super Data> callback) {
    // 读取文件数据
     try {
       data = opener.open(file);
     } catch (FileNotFoundException e) {
       if (Log.isLoggable(TAG, Log.DEBUG)) {
         Log.d(TAG, "Failed to open file", e);
       }
    //失败
       callback.onLoadFailed(e);
       return;
     }
  // 成功
     callback.onDataReady(data);
   }
在这里我们就可以看到根据key读取缓存文件cacheFile,传入File,得到对应的modelloader.fetcher去获取数据,加载完毕后通过,callback.onDataReady(result);把数据回调返回 此callback就是对应的Generator,我们这里是指ResourceCacheGenerator
ResourceCacheGenerator类
 @Override
  public void onDataReady(Object data) {
    cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.RESOURCE_DISK_CACHE,
        currentKey);
  }
继续回调,cb为SourceGenerator
SourceGenerator类
  // Called from source cache generator.
  @Override
  public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher,
      DataSource dataSource, Key attemptedKey) {
    // This data fetcher will be loading from a File and provide the wrong data source, so override
    // with the data source of the original fetcher
    cb.onDataFetcherReady(sourceKey, data, fetcher, loadData.fetcher.getDataSource(), sourceKey);
  }

继续回调cb为DecodeJob

DecodeJob类
  @Override
  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) {
      runReason = RunReason.DECODE_DATA;
      callback.reschedule(this);
    } else {
      TraceCompat.beginSection("DecodeJob.decodeFromRetrievedData");
      try {
        decodeFromRetrievedData();
      } finally {
        TraceCompat.endSection();
      }
    }
  }
//然后判断线程,这里原因上篇文章具体讲解过,最后还是执行decodeFromRetrievedData();
  private void decodeFromRetrievedData() {
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      logWithTimeAndKey("Retrieved data", startFetchTime,
          "data: " + currentData
          + ", cache key: " + currentSourceKey
          + ", fetcher: " + currentFetcher);
    }
    Resource<R> resource = null;
    try {
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
      e.setLoggingDetails(currentAttemptingKey, currentDataSource);
      throwables.add(e);
    }
    if (resource != null) {
      notifyEncodeAndRelease(resource, currentDataSource);
    } else {
      runGenerators();
    }
  }

在onDataReady方法回调给decodeJob的DataSource是DataSource.RESOURCE_DISK_CACHE
通过decodeFromData方法将数据解码成Resource对象后返回即可。然后通过notifyEncodeAndRelease回调UI线程显示出来。
至此,磁盘缓存的读取逻辑完毕

【磁盘缓存的写入】

我们在SourceGenerator这个类的startNext方法触发数据的加载时, loadData.fetcher.loadData(helper.getPriority(), this);加载完毕会返调用SourceGenerator.onDataReady(result);将结果返回
SourceGenerator类
@Override
  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    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 {
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }
  }
此时如果我们的磁盘缓存策略没有禁止,那么 dataToCache = data;同时执行 cb.reschedule();也就是DecodeJob.reschedule():
DecodeJob
public void reschedule() {
    runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
// We might be being called back on someone else's thread. Before doing anything, we should
    // reschedule to get back onto Glide's thread.

    callback.reschedule(this);
  }
callback.reschedule(this);也就是Engine.reschedule();再说一遍原因是我们数据加载完被回调至此,我们可能在其他线程里,但是我们需要切换到Glide自定义的线程。
  @Override
  public void reschedule(DecodeJob<?> job) {
    // Even if the job is cancelled here, it still needs to be scheduled so that it can clean itself
    // up.
    getActiveSourceExecutor().execute(job);
  }
也就是GlideEexcutor的execute,在这里调用DecodeJob的run方法,--runWrapped,因为 runReason = RunReason.SWITCH_TO_SOURCE_SERVICE,所以直接调用 runGenerators();方法。--> 继续,currentGenerator.startNext()这里的代码已经重复很多很多次了,就不过多赘述了。
 @Override
  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++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }
此时调用currentGenerator.startNext()方法dataToCache已经不为null了。也就是cacheData(data);就是这里了,我们在这里写入数据。
private void cacheData(Object dataToCache) {
    long startTime = LogTime.getLogTime();
    try {
// 根据不同的数据获取注册的不同Encoder
      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
      DataCacheWriter<Object> writer =
          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
//得到DiskCache得实现,并存入磁盘。
      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);
  }

至此,磁盘缓存的写入也讲解完毕。

写源码分析真的十分头疼 ☺ ☺ 给个小心心鼓励一下吧~

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

推荐阅读更多精彩内容

  • 引言:2017年8月13日,因感印军越边近两个月,心中激愤,夜不能寐,故作此词明志! 夜来枕上心烦乱,坐起望空叹。...
    李鸿钧阅读 547评论 1 9
  • 大雨滂沱之后, 我的心将更为澄净。 若你肯等待, 所有漂浮不定的云彩, 都将化为幸福的海......。
    月白风清L阅读 144评论 0 2
  • 1 你不在身旁 风都入了我的诗行 于是捣碎了整个寒塘 2 遇见你之后 我这漂泊踉跄的孤舟 得意与失意并立船头 3 ...
    不见得阅读 311评论 0 1
  • 我声称你拥有了我的梦大家都笑了起来笑吧 笑吧我愤怒地哭着你怎么还不来 明明我是唯一的清醒智者怎么他们一笑倒成了跳梁...
    长舌妇阅读 269评论 2 1