阅读 918

源码解析:Glide 4.9之缓存策略

本文Glide源码基于4.9,版本下载地址如下:Glide 4.9

前言

在分析了Glide的图片加载流程后,更加发觉到Glide的强大,于是这篇文章将继续深入分析Glide的缓存策略。不过今天的文章的源码很多基于上一篇加载流程的基础之上,因此还没有看上一篇的小伙伴,建议先去阅读Glide4.9源码解析-图片加载流程效果会更佳哟!

一、设计

1. 二级缓存

  • 内存缓存:基于LruCache和弱引用机制
  • 磁盘缓存:基于DiskLruCache进行封装

Glide有几级缓存?对于这个问题,网上的答案不一,有的认为是五级缓存,也有的认为是三级缓存,但我个人认为是二级缓存,因为个人感觉网络加载并不属于缓存(如果有错误,欢迎在评论指出)

2. 缓存策略

内存缓存-->磁盘缓存-->网络加载

Glide的缓存策略大致是这样的:假设同时开启了内存缓存和磁盘缓存,当程序请求获取图片时,首先从内存中获取,如果内存没有就从磁盘中获取,如果磁盘中也没有,那就从网络上获取这张图片。当程序第一次从网络加载图片后,就将图片缓存到内存和磁盘上。

二、流程

1. 生成缓存key

1.1 作用

缓存key是实现内存和磁盘缓存的唯一标识

1.2 原理

重写equals和hashCode方法,来确保只有key对象的唯一性

1.3 源码解析

生成缓存key的地方其实就在于我们上一篇文章提到的过的Engine的load方法中

Engine#load

  public synchronized <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,
      Executor callbackExecutor) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

	//创建EngineKey对象
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);
    ......
    return new LoadStatus(cb, engineJob);
  }
复制代码

这里会调用keyFactory的buildkey方法来创建EngineKey对象,并将load传入的数据(String,URL等),图片的宽高,签名,设置参数等传进去。

KeyFactory#buildKey

  EngineKey buildKey(Object model, Key signature, int width, int height,
      Map<Class<?>, Transformation<?>> transformations, Class<?> resourceClass,
      Class<?> transcodeClass, Options options) {
    return new EngineKey(model, signature, width, height, transformations, resourceClass,
        transcodeClass, options);
  }
复制代码

可以发现KeyFactory的buildKey方法就是简单的返回了一个EngineKey对象。所以我们来看看这个EngineKey

EngineKey

class EngineKey implements Key {
  private final Object model;
  private final int width;
  private final int height;
  private final Class<?> resourceClass;
  private final Class<?> transcodeClass;
  private final Key signature;
  private final Map<Class<?>, Transformation<?>> transformations;
  private final Options options;
  private int hashCode;

  EngineKey(
      Object model,
      Key signature,
      int width,
      int height,
      Map<Class<?>, Transformation<?>> transformations,
      Class<?> resourceClass,
      Class<?> transcodeClass,
      Options options) {
    this.model = Preconditions.checkNotNull(model);
    this.signature = Preconditions.checkNotNull(signature, "Signature must not be null");
    this.width = width;
    this.height = height;
    this.transformations = Preconditions.checkNotNull(transformations);
    this.resourceClass =
        Preconditions.checkNotNull(resourceClass, "Resource class must not be null");
    this.transcodeClass =
        Preconditions.checkNotNull(transcodeClass, "Transcode class must not be null");
    this.options = Preconditions.checkNotNull(options);
  }

  @Override
  public boolean equals(Object o) {
    if (o instanceof EngineKey) {
      EngineKey other = (EngineKey) o;
      return model.equals(other.model)
          && signature.equals(other.signature)
          && height == other.height
          && width == other.width
          && transformations.equals(other.transformations)
          && resourceClass.equals(other.resourceClass)
          && transcodeClass.equals(other.transcodeClass)
          && options.equals(other.options);
    }
    return false;
  }

  @Override
  public int hashCode() {
    if (hashCode == 0) {
      hashCode = model.hashCode();
      hashCode = 31 * hashCode + signature.hashCode();
      hashCode = 31 * hashCode + width;
      hashCode = 31 * hashCode + height;
      hashCode = 31 * hashCode + transformations.hashCode();
      hashCode = 31 * hashCode + resourceClass.hashCode();
      hashCode = 31 * hashCode + transcodeClass.hashCode();
      hashCode = 31 * hashCode + options.hashCode();
    }
    return hashCode;
  }
  ....
}

复制代码

你会发现在EngineKey中重写equals和hashcode方法,这样就能确保只有传入EngineKey的所有参数都相同的情况下才认为是同一个EngineKey对象。

2. 内存缓存

2.1 作用

防止应用重复将图片数据读取到内存

2.2 原理

缓存原理:弱引用机制LruCache算法

弱引用机制:当JVM进行垃圾回收时,无论当前的内存是否足够,都会回收掉弱引用关联的对象

LruCache算法:内部采用LinkedHashMap以强引用的方式存储外界的缓存对象,当缓存满时,LruCache会移除较早使用的缓存对象,然后添加新的缓存对象

缓存实现:正在使用的图片使用弱引用机制进行缓存,不在使用中的图片使用LruCache来进行缓存。

2.3 配置

Glide默认情况下是开启了内存缓存的,即你不需要做任何处理,只需要通过下面代码正常调用Glide的三部曲即可。

 Glide.with(getContext()).load(url).into(imageView);
复制代码

那我们要关闭内存缓存咋整呢?强大的Glide当然思考了这种问题,Glide提供了非常便捷的API,因此我们只需要通过调用来RequestOptions.skipMemoryCacheOf()并传入true,表示跳过内存缓存,即禁用内存缓存。

 Glide.with(getContext())
        .load(url)
        .apply(RequestOptions.skipMemoryCacheOf(true))
        .into(imageView);
复制代码

2.4 源码解析

在前面我们提到了两种类型的内存缓存,那么Glide是如何协调两者的呢?让我们继续回到Engine的load方法

Engine#load

  public synchronized <R> LoadStatus load(
      ...) {
	//创建EngineKey对象
    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;
    }
    //检查内存的Lrucache缓存是否有目标图片
    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;
    }
    .....  
  }
复制代码

在上面的方法中我们可以发现首先根据宽高,图片URL地址等生成key,然后根据key首先调用loadFromActiveResources获取内存的弱引用缓存的图片,如果获取不到弱引用缓存的图片,才调用loadFromCache获取内存的LruCache缓存。因此我们先来看看Glide对弱引用缓存的操作。

1. 弱引用缓存

1.1 获取

从上面Engine的load方法中,我们知道获取弱引用缓存会调用Engine的loadFromActiveResources方法

Engine#loadFromActiveResources

  private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }
	//重点关注get方法,从弱引用中拿数据
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {
	  //将EngineResource的引用计数加1
      active.acquire();
    }

    return active;
  }
复制代码

这里首先会调用ActiveResources的get方法获取到图片资源,然后将EngineResource的引用计数加一,因为此时EngineResource指向了弱引用缓存的图片资源。

ActiveResources#get

 final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>(); 
 synchronized EngineResource<?> get(Key key) {
    //从弱引用HashMap中取出对应的弱引用对象
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    if (activeRef == null) {
      return null;
    }
    EngineResource<?> active = activeRef.get();
	//如果弱引用中关联的EngineResource对象不存在,即EngineResourse被回收
    if (active == null) {
      //清理弱引用缓存,恢复EngineResource,并保存到LruCache缓存中  
      cleanupActiveReference(activeRef);
    }
    return active;
  }
复制代码

在ActiveResources中的get中,会通过activeEngineResources的get方法得到了数据的弱引用对象,而这个activeEngineResources其实就是个HashMap,所以可以根据key值来找到这个弱引用对象。而我们要找的图片资源其实就是这个弱引用对象关联的对象,让我们来看看ResourceWeakReference的实现

  static final class ResourceWeakReference extends WeakReference<EngineResource<?>> {
    @SuppressWarnings("WeakerAccess") @Synthetic final Key key;
    @SuppressWarnings("WeakerAccess") @Synthetic final boolean isCacheable;

    @Nullable @SuppressWarnings("WeakerAccess") @Synthetic Resource<?> resource;

    @Synthetic
    @SuppressWarnings("WeakerAccess")
    ResourceWeakReference(
        @NonNull Key key,
        @NonNull EngineResource<?> referent,
        @NonNull ReferenceQueue<? super EngineResource<?>> queue,
        boolean isActiveResourceRetentionAllowed) {
      super(referent, queue);
      this.key = Preconditions.checkNotNull(key);
      this.resource =
          referent.isCacheable() && isActiveResourceRetentionAllowed
              ? Preconditions.checkNotNull(referent.getResource()) : null;
      isCacheable = referent.isCacheable();
    }
    ....
  }
}

复制代码

可以发现这个类其实继承了WeakReference,所以当gc发生时,会回收掉ResourceWeakReference对象关联的EngineResource对象,这个对象封装了我们所要获取的图片资源。另外这个类还保存了图片资源和图片资源的缓存key,这是为了当关联的EngineResource对象被回收时,可以利用保存的图片资源来恢复EngineResource对象,然后保存到LruCache缓存中并根据key值从HashMap中删除掉关联了被回收的EngineResource对象的弱引用对象。可以看下EngineResourse被回收的情况

ActiveResources#cleanupActiveReference

  void cleanupActiveReference(@NonNull ResourceWeakReference ref) {
    synchronized (listener) {
      synchronized (this) {
        //将该弱引用缓存从HashMap中删除
        activeEngineResources.remove(ref.key);
        //判断缓存是否可用
        //isCacheable默认情况下为true
        //当配置中设置了RequestOptions.skipMemoryCacheOf()的值的话:
        //1.当skipMemoryCacheOf传入true时为false,即关闭内存缓存
        //2.当skipMemoryCacheOf传入false时为true,即开启内存缓存
        if (!ref.isCacheable || ref.resource == null) {
          return;
        }
		//恢复EngineResource,其中这个对象封装了图片资源
        EngineResource<?> newResource =
            new EngineResource<>(ref.resource, /*isCacheable=*/ true, /*isRecyclable=*/ false);
        newResource.setResourceListener(ref.key, listener);
		//回调,该listener为Engine对象
        listener.onResourceReleased(ref.key, newResource);
      }
    }
  }
复制代码

Engine#onResourceReleased

  public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    //删除弱引用缓存
    activeResources.deactivate(cacheKey);
	//如果开启了内存缓存
    if (resource.isCacheable()) {
	  //将弱引用缓存的数据缓存到LruCache缓存中	
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource);
    }
  }

复制代码

这样当弱引用缓存所关联的图片资源被回收时,会将图片资源保存到LruCache缓存中,从而保证了当获取不到弱引用缓存的图片时,可以获取的到该图片的LruCache缓存。

1.2 存储

弱引用缓存的存储体现在了两个地方:

  • 在主线程展示图片前
  • 获取LruCache缓存时

下面将对这两个地方分别进行解剖!

在主线程展示图片前存储

在讲弱引用缓存的存储前,我们首先要明白这个弱引用缓存保存到图片资源到底是图片的原始数据(图片输入流)还是转换后的图片资源,搞明白的话,找到弱引用存储的地方就不是问题了。这里我就不再细讲如何搞明白这个问题,其中一个思路就是从Engine的load方法中获取到弱引用缓存的操作入手,即回调入手。

    //检查内存弱引用缓存是否有目标图片
	EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
      //此时回调的是SingleRequest的onResourceReady方法
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return null;
    }
复制代码

这个方法其实在上篇文章图片加载流程也提到过,然后追踪下去你就会发现其实最后就是展示这个图片资源,因此我们可以确定这个图片资源应该就是转换后的图片,所以存储弱引用缓存应该是在转换图片后的操作。(这里我直接讲出存储所在的位置,如果想自己深究可以看看上篇文章图片加载流程中的“在主线程中显示图片”这个步骤的代码)最后我们会发现在EngineJob的notifyCallbacksOfResult方法中找到了弱引用缓存入口

EngineJob#notifyCallbacksOfResult

  void notifyCallbacksOfResult() {
    ResourceCallbacksAndExecutors copy;
    Key localKey;
    EngineResource<?> localResource;
    .....
    //内部缓存存储的入口
    //实际上调用的是Engine的onEngineJobComplete
    listener.onEngineJobComplete(this, localKey, localResource);

    for (final ResourceCallbackAndExecutor entry : copy) {
	  //回到主线程展示照片
      entry.executor.execute(new CallResourceReady(entry.cb));
    }

	//通知上层删除弱引用缓存数据
    decrementPendingCallbacks();
  }


复制代码

没错其入口就是我们在Glide图片加载流程提到过的回到主线程展示照片代码的前面,即回调了Engine的onEngineJobComplete来存储弱引用缓存

Engine#onEngineJobComplete

  public synchronized void onEngineJobComplete(
      EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    if (resource != null) {
      resource.setResourceListener(key, this);
      //如果开启内存缓存的话,将解析后的图片添加到弱引用缓存
      if (resource.isCacheable()) {
        activeResources.activate(key, resource);
      }
    }

    jobs.removeIfCurrent(key, engineJob);
  }
复制代码

ActiveResources#activate

  synchronized void activate(Key key, EngineResource<?> resource) {
    //构建弱引用对象
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
    //将获取到的缓存图片存储到弱引用对象的HashMap中
    //key值不重复返回null,key值重复返回旧对象
    ResourceWeakReference removed = activeEngineResources.put(key, toPut);
    if (removed != null) {
      //如果key值重复,就将之前的弱引用对象的图片资源置为null  
      removed.reset();
    }
  }
复制代码

从这里也可以得到一个结论:正在使用的图片会存储到弱引用缓存中而不是LruCache缓存

获取LruCache缓存时存储

由于这个操作同时也涉及了LruCache的获取,故可以直接看下面对LruCache获取的解析

1.3 删除

弱引用缓存的删除其实体现在两处:

  • JVM进行GC时
  • 弱引用缓存对象引用计数为0时

JVM进行GC时

当JVM进行GC时,由于弱引用对象的特性,导致了弱引用缓存所关联的对象也会被回收,然后就会删除掉这个弱引用缓存对象,这部分我们在弱引用缓存获取的时候也分析过,这里不再进行解析(忘记的可以回头看看前面弱引用获取的分析)。

弱引用缓存对象引用计数为0时

细心的你不知有没有发现,其实在上面对缓存入口的分析时其实已经贴出了弱引用缓存删除的代码语句。不过为了方便阅读,在这里我还是再次直接贴出来。

EngineJob#notifyCallbacksOfResult

    //内部缓存存储的入口
    //实际上调用的是Engine的onEngineJobComplete
    listener.onEngineJobComplete(this, localKey, localResource);

    for (final ResourceCallbackAndExecutor entry : copy) {
	  //回到主线程展示照片
      entry.executor.execute(new CallResourceReady(entry.cb));
    }

	//通知上层删除弱引用缓存数据
    decrementPendingCallbacks();
复制代码

EngineJob#decrementPendingCallbacks

  synchronized void decrementPendingCallbacks() {
    .....
    if (decremented == 0) {
      if (engineResource != null) {
	  	//重点关注
        engineResource.release();
      }
      release();
    }
  }
复制代码

这里调用了EngineResource的release方法,让我们来看看

EngineResource#release

  void release() {
    synchronized (listener) {
      synchronized (this) {
        if (acquired <= 0) {
          throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
        }
		//每次调用release内部引用计数法减一,当为0时,代表没引用,通知上层回收
        if (--acquired == 0) {
		  //回调,listener为Engine类型	
          listener.onResourceReleased(key, this);
        }
      }
    }
  }
复制代码

在这里使用了著名的判断对象是否存活的算法-引用计数法,每次调用EngineResource对象的release方法,都会令该引用减一,当引用计数为0时,表示已经不再使用该对象,即图片不再使用时,就会回调Engine的onResourceReleased方法

Engine#onResourceReleased

  public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    //删除弱引用缓存
    activeResources.deactivate(cacheKey);
	//如果开启了内存缓存
    if (resource.isCacheable()) {
	  //将弱引用缓存的数据缓存到LruCache缓存中	
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource);
    }
  }
复制代码

跟上面存储弱引用缓存时提到的发生GC的情况一样,最终会删除弱引用缓存,然后将该图片资源添加到LruCache缓存中。从这里也可以验证了我们上文提到的内存缓存的原理中的缓存实现:正在使用的图片使用弱引用机制进行缓存,不在使用中的图片使用LruCache来进行缓存。

2. LruCache缓存

2.1 获取

上文我们提到,获取内存缓存时,如果获取不到弱引用缓存时才会调用loadFromCache获取LruCache缓存。让我们看看Engine的loadFromCache方法

Engine#loadFromCache

  private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
     //isMemoryCacheable默认情况下为true
     //当配置中设置了RequestOptions.skipMemoryCacheOf()的值的话:
     //1.当skipMemoryCacheOf传入true时为false,即关闭内存缓存
     //2.当skipMemoryCacheOf传入false时为true,即开启内存缓存
    if (!isMemoryCacheable) {
      return null;
    }
    //获取图片缓存,并将该缓存从LruCache缓存中删除
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
	  //将EngineResource的引用计数加1	
      cached.acquire();
	  //将内存缓存存入弱引用缓存中
	  //好处:保护这些图片不会被LruCache算法回收掉
      activeResources.activate(key, cached);
    }
    return cached;
  }
复制代码

获取LruCache缓存跟弱引用缓存的获取操作很相似,首先调用了getEngineResourceFromCache来获取图片资源,然后将EngineResource的引用计数加1,并且还会将获取到的图片资源存储到弱引用缓存中。这里我们只分析getEngineResourceFromCache方法,因为调用ActiveResource的activate存储到弱引用缓存我们已经在上面弱引用缓存的存储中分析过了。

EngineResource#getEngineResourceFromCache

  /**
   * 作用:获取图片缓存
   * 过程:根据缓存key从cache中取值
   * 注:此cache对象为Glide构建时创建的LruResourceCache对象,说明使用的是LruCache算法
   */
  private EngineResource<?> getEngineResourceFromCache(Key key) {
    //当获取到缓存图片时,从缓存中移除
    Resource<?> cached = cache.remove(key);

    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就是LruCache缓存的cache,另外你会发现获取图片缓存竟然不是调用cache的get方法,而是cache的remove方法,这就是Glide缓存策略的奇妙之处了。当获取到LruCache缓存的同时会删除掉该LruCache缓存,然后将该缓存存储到弱引用缓存中,这是为了保护这些图片不会被LruCache算法回收掉。

2.2 存储

当弱引用缓存删除时,会将缓存存储到LruCache缓存中。(分析可以看弱引用缓存删除操作)

2.3 删除

当获取LruCache缓存的同时对该LruCache缓存进行删除操作。(分析可以看LruCache缓存的获取操作)

2.5 小结

分析完内存缓存,你会发现弱引用缓存和LruCache缓存真的是环环相扣,密不可分,很多操作都是有关联性的。其流程图如下:

流程图的前提:开启内存缓存,关闭磁盘缓存

3. 磁盘缓存

3.1 作用

防止应用重复的从网络或从其它地方下载和读取数据

3.2 原理

使用Glide自定义的DiskLruCache算法

DiskLruCache算法是基于LruCache算法,该算法的应用场景是存储设备的缓存,即磁盘缓存。

3.3 配置

磁盘缓存也是默认开启的,默认情况下磁盘缓存的类型为DiskCacheStrategy.AUTOMATIC,当然可以通过代码关闭或者选择其它类型的磁盘缓存

 Glide.with(getContext())
        .load(url)
	    .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.RESOURCE))
        .into(imageView);
复制代码

diskCacheStrategyOf的相关参数说明如下:

参数 说明
DiskCacheStrategy.AUTOMATIC
这是默认的最优缓存策略:
本地:仅存储转换后的图片(RESOURCE)
网络:仅存储原始图片(DATA)。因为网络获取数据比解析磁盘上的数据要昂贵的多
DiskCacheStrategy.NONE 不开启磁盘缓存
DiskCacheStrategy.RESOURCE 缓存转换过后的图片
DiskCacheStrategy.DATA 缓存原始图片,即原始输入流。它需要经过压缩转换,解析等操作才能最终展示出来
DiskCacheStrategy.ALL 既缓存原始图片,又缓存转换后的图片

注:Glide加载图片默认是不会将一张原始图片展示出来的,而是将原始图片经过压缩转换,解析等操作。然后将转换后的图片展示出来。

3.4 源码解析

下列源码解析的前提:开启了磁盘缓存

虽然说上面的参数有五种,但其实我们只需要分析其中两种就能理解其它参数了。没错,接下来我们将分析RESOURCE类型和DATA类型。在分析前我们先回顾下Engine的load方法

Engine#load

  public synchronized <R> LoadStatus load(
      .....) {
      
      
     ......
	//创建EngineKey对象
    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;
    }
    //检查内存中Lrucache缓存是否有目标图片
    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;
    }

    //走到这表示内存中没有图片,则开启新线程加载图片
	
	........
    //创建EngineJob对象,用来开启线程(异步加载图片)
    EngineJob<R> engineJob =
        engineJobFactory.build(
            key,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache);
    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, callbackExecutor);
    engineJob.start(decodeJob);
    return new LoadStatus(cb, engineJob);
  }

复制代码

从上面可以发现如果获取不到内存缓存时,会开启线程来加载图片。从上篇文章Glide 4.9源码解析-图片加载流程我们可以知道,接下来会执行DecodeJob的run方法。

DecodeJob

  public void run() {
      .....
	  //重点关注,调用runWrapped
      runWrapped();
  }

  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 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);
    }
  }
复制代码

可以发现在上述的方法中首先要找到对于场景的执行者然后执行任务。而执行者有三个,在上篇文章我们分析的是无缓存的情况,即网络获取数据的执行者。接下来我们就得分析获取转换后图片的执行者和获取原始突破的执行者。

1. RESOURCE缓存(转换图片)

1.1 获取

由于我们现在配置的缓存策略为RESOURCE,故对于执行者将是获取转换图片的执行者ResourceCacheGenerator,接下来会执行者会执行任务,让我们看看runGenerators方法

Engine#runGenerators

  private void runGenerators() {
    currentThread = Thread.currentThread();
    startFetchTime = LogTime.getLogTime();
    boolean isStarted = false;
	// 调用 currentGenerator.startNext() 执行了请求操作
	//我们这里主要分析的是无缓存情况,所以这里的currentGenerator应该是ResourceCacheGenerator
    while (!isCancelled && currentGenerator != null
        && !(isStarted = currentGenerator.startNext())) {
      stage = getNextStage(stage);
      currentGenerator = getNextGenerator();

      if (stage == Stage.SOURCE) {
        reschedule();
        return;
      }
    }
复制代码

因为我们现在的执行者为ResourceCacheGenerator,所以会调用ResourceCacheGenerator的startNext来进行获取图片。

ResourceCacheGenerator#startNext

  public boolean startNext() {
    ....
    while (modelLoaders == null || !hasNextModelLoader()) {
      .....

      Key sourceId = sourceIds.get(sourceIdIndex);
      Class<?> resourceClass = resourceClasses.get(resourceClassIndex);
      Transformation<?> transformation = helper.getTransformation(resourceClass);
      //构建获取缓存数据的key,这个key中传入了图片大小,变换等参数
      //即根据各种变换的条件获取缓存数据,故这个执行者就是用来获取变换之后的缓存数据
      currentKey =
          new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoops
              helper.getArrayPool(),
              sourceId,
              helper.getSignature(),
              helper.getWidth(),
              helper.getHeight(),
              transformation,
              resourceClass,
              helper.getOptions());
	  //从缓存中获取缓存信息
	  //首先通过getDiskCache获取DiskCache对象
	  //然后通过key获取到转换过后的资源
      cacheFile = helper.getDiskCache().get(currentKey);
      if (cacheFile != null) {
        sourceKey = sourceId;
		//该modeLoaders的类型为File类型的
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
	  //获取数据加载器
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
	  //构建一个加载器,构建出来的是ByteBufferFileLoader
      loadData = modelLoader.buildLoadData(cacheFile,
          helper.getWidth(), helper.getHeight(), helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
		//调用了ByteBufferFileLoader的内部类ByteBufferFetcher的loadData
		//最后会把结果回调给DecodeJob的onDataFetcherReady
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }

    return started;
  }
复制代码

可以发现在ResourceCacheGenerator的startNext方法中,首先根据图片的参数,宽高等信息拿到缓存key,然后通过缓存key获取到磁盘上的文件,即磁盘缓存。最后通过加载器的loadData来处理获取到的磁盘缓存,由于磁盘缓存是File类型,根据Glide的注册表registry中可以找到该加载器其实是ByteBufferFileLoader(具体查找可看上篇博客的分析),故最后会调用ByteBufferFileLoader内部类ByteBufferFetcher的loadData方法来处理磁盘缓存。

ByteBufferFileLoader.ByteBufferFetcher#loadData

    public void loadData(@NonNull Priority priority,
        @NonNull DataCallback<? super ByteBuffer> callback) {
      ByteBuffer result;
      try {
	  	//获取对应磁盘上的转换过后的数据
        result = ByteBufferUtil.fromFile(file);
      } catch (IOException e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "Failed to obtain ByteBuffer for file", e);
        }
        callback.onLoadFailed(e);
        return;
      }
	  //此callback为ResourceCacheGenerator	
      callback.onDataReady(result);
    }
复制代码

在loadData中会通过ByteBufferUtil工具类来获取对应磁盘文件的数据,然后通过回调ResourceCacheGenerator的onDataReady方法将数据回调出去。

ResourceCacheGenerator#onDataReady

  public void onDataReady(Object data) {

    //此时的cb为DecodeJob,即调用了DecodeJob的onDataFetcherReady
    cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.RESOURCE_DISK_CACHE,
        currentKey);
  }
复制代码

进行回调DecodeJob的onDataFetcherReady

DecodeJob#onDataFetcherReady

  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 {
      GlideTrace.beginSection("DecodeJob.decodeFromRetrievedData");
      try {
	  	//解析获取的数据
        decodeFromRetrievedData();
      } finally {
        GlideTrace.endSection();
      }
    }
  }
复制代码

接下来就会对数据进行压缩转换等操作,然后进行展示(在上篇文章已经分析,这里不再进行后续的分析)。

1.2 存储

由于我们分析的是转换后的图片的存储,故其存储位置应该是在对原始图片压缩转换解析等一系列操作完后进行的,根据上一篇文章的分析,我们直接看DecodeJob的decodeFromRetrievedData方法

DecodeJob

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

  private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) {
    .....
    //图片加载流程时重点关注的地方
    notifyComplete(result, dataSource);

    stage = Stage.ENCODE;
    try {
	  //是否可以将转换的图片缓存
      if (deferredEncodeManager.hasResourceToEncode()) {
	  	 //磁盘缓存入口	
        deferredEncodeManager.encode(diskCacheProvider, options);
      }
    } finally {
      if (lockedResource != null) {
        lockedResource.unlock();
      }
    }
    onEncodeComplete();
  }
复制代码

我们在通知外界资源获取成功即notifyEncodeAndRelease方法中发现了RESOURCE类型的磁盘缓存的入口。

DecodeJob.DeferredEncodeManager#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();
      }
    }
复制代码

在encode方法中通过DiskCacheProvider获取到DiskCache,然后调用put方法将图片资源缓存到磁盘上。

1.3 删除

由于缓存在了磁盘上,故删除不仅仅由代码控制。常见的删除方式如下:

  • 用户主动删除手机上的对应文件
  • 卸载软件
  • 手动调用Glide.get(applicationContext).clearDiskCache()

2. DATA缓存(原始图片)

2.1 获取

通过上面的分析我们知道原始图片对应的执行者为DataCacheGenerator,故还是会调用DataCacheGenerator的startNext方法来获取磁盘缓存

DataCacheGenerator#startNext

  public boolean startNext() {
    while (modelLoaders == null || !hasNextModelLoader()) {
      .....
      Key sourceId = cacheKeys.get(sourceIdIndex);
      @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
      //构建缓存key  
      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
      //获取缓存key的磁盘资源
      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的startNext几乎是一样,不同的是缓存key的构建参数是不一样的,因为原始图片的缓存key是不需要图片的宽高,配置,变换等参数。然后接下来的分析与转换图片获取的分析是一致的,这里不再进行分析。

2.2 存储

我们知道原始图片说白了就是网络获取后得到的原始的输入流,通过上一篇加载流程的分析,我们知道获取到原始输入流是在HttpUrlFetcher的loadData方法中

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());
	  //将inputStream回调出去,回调了SourceGenerator的onDataReady
      callback.onDataReady(result);
    } catch (IOException e) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Failed to load data for url", e);
      }
      callback.onLoadFailed(e);
    } finally {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime));
      }
    }
  }
复制代码

在获取到原始输入流后,会调用SourceGenerator的onDataReady将输入流回调出去

SourceGenerator#onDataReady

  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
	//如果开启了磁盘缓存
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
	  //将网络获取到的原始数据,赋值给dataToCache	
      dataToCache = data;
      //调用DecodeJob的reschedule,用线程池执行任务,实际上就是再次调用SourceGenerator的startNext
      cb.reschedule();
    } else {
	  //没有开启磁盘缓存
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }
  }
复制代码

由于我们开启了磁盘缓存,故会将原始数据赋值给dataToCache,然后回调了DecodeJob的reschedule。

DecodeJob#reschedule

  public void reschedule() {
    runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
	//此时的callback为EngineJob
    callback.reschedule(this);
  }
复制代码

这里继续回调了EngineJob的reschedule方法

EngineJob#reschedule

  public void reschedule(DecodeJob<?> job) {
    //再次切换到网络线程,执行DecodeJob的run方法
    getActiveSourceExecutor().execute(job);
  }
复制代码

这里的job为DecodeJob,而getActiveSourceExecutor()会拿到线程池,所以reschedule方法其实会继续执行DecodeJob的run方法,然后拿到网络获取数据的执行者SourceGenerator,再次执行SourceGenerator的startNext方法(考虑到篇幅,不再贴其中的流程代码了,详细可以看上篇文章Glide 4.9源码解析-图片加载流程

不过估计在这里可能有人会疑问,我们明明开启了磁盘缓存,为什么会获取到无缓存,网络获取数据的执行者呢?这是因为我们在存储原始图片的前提下,肯定是磁盘没有缓存,因此会从网络加载图片得到原始图片的输入流,然后回调,回调后当然还是拿到网络获取数据的执行者SourceGenerator。让我们再来看看这个startNext方法。

SourceGenerator#startNext

  public boolean startNext() {
     //第二次进入
    //现在dataToCache不等于null,为原始图片
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
	  //放入缓存 
      cacheData(data);
    }
    //当原始图片放入磁盘缓存后,sourceCacheGenerator为DataCacheGenerator
    //然后继续执行DataCacheGenerator的startNext方法
    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;
  }
复制代码

通过上面的分析我们知道此时的dataToCache是不为null的,而是原始图片,所以会调用cacheData方法将原始图片放入到磁盘缓存中。(如果你阅读了Glide图片加载流程的话,就会发现我们在图片加载流程的时候分析的其实是下面的代码,即没有开启缓存或获取不到磁盘缓存的情况)这里我们继续看cacheData方法

SourceGenerator#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());
      //构建缓存key  
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());

	  //存储原始图片
      helper.getDiskCache().put(originalKey, writer);
      ......
    } finally {
      loadData.fetcher.cleanup();
    }
    //构造一个DataCacheGenerator对象,用来加载刚保存的磁盘缓存
    sourceCacheGenerator =
        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
  }
复制代码

在cacheData方法中会通过DiskCache的put方法将缓存key,原始图片等存储到磁盘中。然后会构造DataCacheGenerator对象,这时候我们看回SourceGenerator的startNext方法,由于此时的sourceCacheGenerator已经是DataCacheGenerator对象了,所以会调用DataCacheGenerator的startNext方法来获取磁盘缓存中的原始图片。

2.3 删除

由于原始图片的缓存也属于磁盘缓存,故跟RESOURCE缓存一样删除不仅仅由代码控制,常见删除方式如下:

  • 用户主动删除手机上的对应文件
  • 卸载软件
  • 手动调用Glide.get(applicationContext).clearDiskCache()

3.5 小结

从上面的分析可以发现Glide中首先会读取转换后的图片的缓存,然后再读取原始图片的缓存。但是存储的时候恰恰相反,首先存储的是原始图片的缓存,再存储转换后的图片,不过获取和存储都受到Glide使用API的设置的影响。其流程图如下:

流程图的前提:关闭内存缓存或获取不到内存缓存,开启磁盘缓存

总结

通过对Glide缓存策略的分析,发现了Glide的缓存机制是多么的复杂,但又是多么的出色啊,所以用起来才会这么流畅和舒服。分析到这,Glide的缓存策略也就讲完了,而对Glide这个强大的图片开源库的源码分析也告一段落了,通过对Glide的图片加载流程和缓存策略的源码解析,让我更加佩服Glide这个强大的开源库。

参考文章:

关注下面的标签,发现更多相似文章
评论