Android Glide 源码剖析 缓存篇

前语

Glide作为在Android中广泛运用的图片加载库为人所熟悉. 最近在项目中刚好遇到一个与Glide缓存战略相关的问题并且也一向想收拾关于源码的剖析文章. 所以写下了这篇文章.

本文所用Glide版本为: 4.16.0 阅读本篇前了解以下几点能够帮助更好的了解

  1. Java 引证类型以及引证行列
  2. Lru算法原理
  3. Bitmap根本知识
  4. MessageDigest摘要算法

Key的生成

Glide中专门界说了Key接口用于标识数据, 其中后两个函数咱们都很熟悉用于生成hashCode以及目标比较. 第一个接口可能就比较陌生了. 它首要作用是在磁盘缓存时, 将所有能作为仅有标识的信息生成摘要, 后续在磁盘缓存时会介绍到. 下面先来看一下内存缓存时运用的Key.

public interface Key {
  String STRING_CHARSET_NAME = "UTF-8";
  Charset CHARSET = Charset.forName(STRING_CHARSET_NAME);
  /**
   * Adds all uniquely identifying information to the given digest.
   */
  void updateDiskCacheKey(@NonNull MessageDigest messageDigest);
  @Override
  boolean equals(Object o);
  @Override
  int hashCode();
}

当开始加载资源时, Glide会优先测验从内存缓存中加载数据. 依据当时资源的地址以及加载的相关设置生成对应的Key来对资源进行查询.

// An in memory only cache key used to multiplex loads.
// (仅在内存缓存中运用的Key, 用于重复加载)
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; //额定校验的Key
  private final Map<Class<?>, Transformation<?>> transformations;
  private final Options options;
  private int hashCode;
  @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;
  }
}

以上代码便是Key的组成, 在项目中假如咱们没有对图片加载做动态设置的话.每张图片对应的Key会一向坚持不变.

比较值得注意的一点是signature变量, 默许状况下它的值为EmptySignature的单例目标不会影响Key的比较. 该变量相当于Glide留给开发者对图片Key修正的接口. 在某些状况下, 会呈现图片现已变动但Key却仍然共同的状况. 下面会介绍这种过错

EngineKey的过错

假设当时存在一张图片运用Glide进行加载, 在加载完成后对图片进行替换(图片地址不变), 然后重新加载图片. 那么这时候仍然会显现上一张图片. 原因是因为生成Key的相关信息没有任何改变, 前后生成的key相同导致复用了过错的缓存

复现方式: 运用adb push将不同的两张图片先后push进SD卡, 当push第一张后进行加载, 然后push第二张将前一张进行覆盖, 然后再触发Glide加载. 观察第2次加载是否能加载出第二张图片

结果是第二张图片并不会被加载, 显现的仍然是第一张图片

解决方案:

val file = File(PATH)
Glide.with(this@EngineKeyActivity)
    .load(file)
    //添加文件终究修正时间作为signature 前后生成的Key自然不同 不会过错复用图片缓存
    .signature(ObjectKey(file.lastModified())) 
    .into(binding.img)

以上是针对本地图片加载的修正方案. 详细还是要依据事务场景来作修正, 这里还是进行过错剖析以及提供思路.

Glide Resource 资源包装类

在了解Glide 缓存完成之前, 咱们先来了解一下Glide的资源包装接口. 这与后面介绍Glide缓存机制直接相关

/**
 * A resource interface that wraps a particular type so that it can be pooled and reused.
 * Type parameters:<Z> – The type of resource wrapped by this class.
 * 翻译: 包装特定资源类型的接口, 以此来完成资源的池化和复用, Z代表被包装的资源类
 */
public interface Resource<Z> {
  @NonNull
  Class<Z> getResourceClass();
  @NonNull
  Z get();
  int getSize();
  void recycle();
}

在Glide内部对各种资源类型都做了包装, 以此来完成对资源的办理. 池化以及复用首要是针对Bitmap. 后续有时机再独自讲解.

EngineResource

在各种Resource的完成类中, 有个相对特别的完成类EngineResource. 它首要作用是在其它Resource的基础上再添加一层包装, 内部经过引证计数的方式对当时Resource进行办理与Glide的内存缓存休戚相关.

class EngineResource<Z> implements Resource<Z> {
  private final boolean isMemoryCacheable; //是否可内存缓存
  private final boolean isRecyclable; //是否可收回
  private final Resource<Z> resource; //真正运用的资源的包装类
  private final ResourceListener listener; //资源开释回调
  private final Key key; //对应Key
  private int acquired; //引证计数
  private boolean isRecycled; //是否已收回
  interface ResourceListener {
    (剧透: 后文伏笔) 
    void onResourceReleased(Key key, EngineResource<?> resource);
  }
  // 省略部分代码....
  // 资源收回 实际上是调用resource.recycle()进行收回, EngineResource首要是添加一些判别 
  @Override
  public synchronized void recycle() {
    if (acquired > 0) {
      throw new IllegalStateException("Cannot recycle a resource while it is still acquired");
    }
    if (isRecycled) {
      throw new IllegalStateException("Cannot recycle a resource that has already been recycled");
    }
    isRecycled = true;
    if (isRecyclable) {
      resource.recycle();
    }
  }
  //引证计数  
  synchronized void acquire() {
    if (isRecycled) {
      throw new IllegalStateException("Cannot acquire a recycled resource");
    }
      acquired;
  }
  //引证计数-- 判别引证计数是否为0 为0 则回调onResourceReleased接口
  void release() {
    boolean release = false;
    synchronized (this) {
      if (acquired <= 0) {
        throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
      }
      if (--acquired == 0) {
        release = true;
      }
    }
    if (release) {
      listener.onResourceReleased(key, this); //告诉资源开释
    }
  }
}

引证计数的首要作用

acquire()

从以上注释中能够看出引证计数首要代表了当时Resource运用者的数量. 该函数首要是在射中缓存或资源加载成功时调用. 代表运用者数量 1

release()

release则相反, 代表当时资源已不再被运用, 当引证计数抵达0时, 则告诉资源已被开释. 实际这里是缓存战略中重要的一步, 当onResourceReleased调用后, resource将会从ActiveResources中移除参加到LruResourceCached中.

Glide缓存战略

经过上面的铺垫, 下面能够来为大家介绍Glide内部的缓存逻辑了

在Glide内部缓存首要被分为了三个部分

  1. ActiveResources(活动缓存)
  2. LruResourceCache(Lru战略缓存)
  3. DiskCache(磁盘缓存)

其中ActiveResourcesLruResourceCache都属于内存缓存, 而之所以这样区别是有重要原因的.

假设Glide去掉ActiveResources而只运用LruResourceCache的话, 因为Lru的完成是固定缓存数量移除最近最少被运用的缓存, 那么当缓存数量到达满时那么就会有resource被移除. 下面看一下被移除的resource详细会履行什么逻辑

@Override
public void onResourceRemoved(@NonNull final Resource<?> resource) {
  resourceRecycler.recycle(resource, true); //直接履行资源的收回
}

LruResourceCache中移除的缓存会调用recycler()进行收回 终究是调用到resource.recycle(). 各种resource的完成不同, Bitmap的话终究实际上会被放回BitmapPool中用于Bitmap复用.

从以上能够看出, 假如将内存缓存都放在LruResourceCache的话, 无法很好的办理正在运用的缓存与没有正在运用的缓存. 因为当缓存数量到达满时, 可能会存在正在被运用的缓存被移除收回的状况. 因而Glide将内存缓存分为了两部分, 实际上便是将当时正在运用的缓存与没有在运用的缓存做了区别办理. 而ActiveResources内部缓存的正是当时正在被运用的缓存

下面来看一下ActiveResources是怎么办理当时正在运用的缓存的

ActiveResources(活动缓存)

因为代码较长, 所以删去大部分不太重要的代码. 引荐大家能够合作源码比照食用

final class ActiveResources {
  private final boolean isActiveResourceRetentionAllowed; //是否再持有resource引证 详细看ResourceWeakReference
  private final Executor monitorClearedResourcesExecutor; 
  final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>(); 
  private final ReferenceQueue<EngineResource<?>> resourceReferenceQueue = new ReferenceQueue<>(); // 引证行列
  private ResourceListener listener;
  private volatile boolean isShutdown;
  ActiveResources(
      boolean isActiveResourceRetentionAllowed, Executor monitorClearedResourcesExecutor) {
    this.isActiveResourceRetentionAllowed = isActiveResourceRetentionAllowed;
    this.monitorClearedResourcesExecutor = monitorClearedResourcesExecutor;
    // 单一线程池 线程设置低优先级 整理引证行列中的无用缓存
    monitorClearedResourcesExecutor.execute(
        new Runnable() {
          @Override
          public void run() {
            cleanReferenceQueue();
          }
        });
  }
  // 参加新的缓存
  synchronized void activate(Key key, EngineResource<?> resource) {
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
    ResourceWeakReference removed = activeEngineResources.put(key, toPut); //将原先的资源开释
    if (removed != null) {
      removed.reset();
    }
  }
  // 整理无效缓存
  synchronized void deactivate(Key key) {
    ResourceWeakReference removed = activeEngineResources.remove(key);
    if (removed != null) {
      removed.reset();
    }
  }
  // 获取缓存复用
  synchronized EngineResource<?> get(Key key) {
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    if (activeRef == null) {
      return null;
    }
    EngineResource<?> active = activeRef.get();
    if (active == null) {
      cleanupActiveReference(activeRef);
    }
    return active;
  }
  // 整理Map
  void cleanupActiveReference(@NonNull ResourceWeakReference ref) {
    synchronized (this) {
      activeEngineResources.remove(ref.key);
      if (!ref.isCacheable || ref.resource == null) {
        return;
      }
    }
    EngineResource<?> newResource =
        new EngineResource<>(
            ref.resource, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ false, ref.key, listener);
    listener.onResourceReleased(ref.key, newResource);
  }
  // 在关闭缓存前会一向整理引证行列中已被收回的EngineResource
  void cleanReferenceQueue() {
    while (!isShutdown) {
      try {
        ResourceWeakReference ref = (ResourceWeakReference) resourceReferenceQueue.remove();
        cleanupActiveReference(ref);
    }
  }
  //继承弱引证指向EngineResource
  static final class ResourceWeakReference extends WeakReference<EngineResource<?>> {
    final Key key;
    final boolean isCacheable; //是否可内存缓存
    Resource<?> resource; // 弱引证中再一次持有资源引证, 默许完成是为null
    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.isMemoryCacheable() && isActiveResourceRetentionAllowed
              ? Preconditions.checkNotNull(referent.getResource())
              : null;
      isCacheable = referent.isMemoryCacheable();
    }
    void reset() {
      resource = null;
      clear();
    }
  }
}

这一部分代码非常长咱们一步步来剖析

从上面代码能够看出以下几点

  1. ActiveResources直接缓存EngineResource类型
  2. 经过Map WeakReference ReferenceQueue的方式对缓存进行存储, 开启Thread循环整理已被收回目标
  3. 经过activate deactivate get 参加 移除 获取缓存

仅经过以上无法看出ActiveResources是怎么与当时缓存是否正在运用相关的, 下面来看一下activate deactivate get的调用途径

// 测验从ActiveResource缓存中获取资源
private EngineResource<?> loadFromActiveResources(Key key) {
  EngineResource<?> active = activeResources.get(key); //获取缓存
  if (active != null) {
    active.acquire(); //引证计数  
  }
  return active;
}
// 射中缓存, 将新创建的资源参加到ActiveResource
private EngineResource<?> loadFromCache(Key key) {
  EngineResource<?> cached = getEngineResourceFromCache(key);
  if (cached != null) {
    cached.acquire(); //引证计数  
    activeResources.activate(key, cached); //参加缓存
  }
  return cached;
}
// 当EngineResource引证计数为0时回调
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
  activeResources.deactivate(cacheKey); //移除缓存
  if (resource.isMemoryCacheable()) {
    cache.put(cacheKey, resource); // 若可运用内存缓存, 则将当时resource参加到LruResourceCache
  } else {
    resourceRecycler.recycle(resource, /* forceNextFrame= */ false); // 否则直接收回
  }
}

从以上能够看出Glide关于当时正在运用的资源办理首要是经过EngineResource内部的引证计数来完成的. 当资源加载成功时将其参加到ActiveResources中, 假如后续再次射中缓存则引证计数 . 当引证计数下降为0时则从中移除.

LruResourceCache

关于LruResourceCache其实没有太多能够介绍的, 在上面介绍ActiveResources时根本现已介绍过了, 在这里再说明一下.

LruResourceCache内部运用Lru算法进行缓存办理. 当缓存到达满时, 会将最近最少运用的资源Resource目标移除并且调用Resource.recycler()收回资源.

DiskCache

关于磁盘缓存, Glide同样是根据Lru算法完成, 并且内部运用了战略形式制定了多种战略, 下面来看一下战略抽象类中界说的接口

public abstract class DiskCacheStrategy {
  // 回来true 代表应该存储原始数据
  public abstract boolean isDataCacheable(DataSource dataSource);
  //回来true代表应该存储解码后的数据
  public abstract boolean isResourceCacheable(
      boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy);
  // 回来true 代表当时请求应该测验对已缓存的改换后的数据解码
  public abstract boolean decodeCachedResource();
  // 回来true代表当时请求应该测验对已缓存的原始数据解码
  public abstract boolean decodeCachedData();
}

编码战略 EncodeStrategy

public enum EncodeStrategy {
  /**
   * Writes the original unmodified data for the resource to disk, not include downsampling or
   * transformations.
   * 将未经修正的数据写入磁盘
   */
  SOURCE,
  /** 
   * Writes the decoded, downsampled and transformed data for the resource to disk.
   * 将解码 采样 改换后的数据写入磁盘
   */
  TRANSFORMED,
  /** 
   * 啥都不写
   */
  NONE,
}

数据来历DataSource

public enum DataSource {
  // 本地
  LOCAL,
  // 服务端
  REMOTE,
  // 本地原始数据缓存
  DATA_DISK_CACHE,
  // 本地改换后的数据缓存
  RESOURCE_DISK_CACHE,
  // 内存缓存
  MEMORY_CACHE,
}

详细的DiskCacheStrategy战略完成类分为以下几种

  • ALL: 缓存服务端原始数据以及解码后的数据
  • NONE: 统统不存
  • DATA: 仅缓存解码前的原始数据
  • RESOURCE: 仅缓存解码后的数据
  • AUTOMATIC: 默许战略 缓存服务端原始数据, 关于本地数据缓存改换后的数据

从以上能够看出, Glide关于磁盘缓存也是做了两种缓存类型进行存储. 分别是原始数据类型以及解码转换后的数据类型.

关于两种缓存类型分别用了两种Key DataCacheKeyResourceCacheKey来进行映射. 关于两种Key就不多介绍了, DataCacheKey其实是是对当时请求数据的Key以及开发者自界说signature的包装, ResourceCacheKey则在前者的基础上添加了解码以及改换相关的属性.

下面来看一下Key是怎么映射到本地文件的

public class DiskLruCacheWrapper implements DiskCache {
    @Override
    public File get(Key key) {
        String safeKey = safeKeyGenerator.getSafeKey(key); // 生成String类型Key
        File result = null;
        try {
            final DiskLruCache.Value value = getDiskCache().get(safeKey);
            if (value != null) {
                result = value.getFile(0);
            }
        } catch (IOException e) {
            if (Log.isLoggable(TAG, Log.WARN)) {
                Log.w(TAG, "Unable to get from disk cache", e);
            }
        }
        return result;
    }
}
public class SafeKeyGenerator {
  public String getSafeKey(Key key) {
    String safeKey;
    synchronized (loadIdToSafeHash) {
      safeKey = loadIdToSafeHash.get(key); // Key的Lru缓存减少重复核算
    }
    if (safeKey == null) {
      safeKey = calculateHexStringDigest(key); //Key的核算
    }
    synchronized (loadIdToSafeHash) {
      loadIdToSafeHash.put(key, safeKey);
    }
    return safeKey;
  }
  private String calculateHexStringDigest(Key key) {
    PoolableDigestContainer container = Preconditions.checkNotNull(digestPool.acquire());
    try {
      key.updateDiskCacheKey(container.messageDigest); //核算签名
      // calling digest() will automatically reset()
      return Util.sha256BytesToHex(container.messageDigest.digest()); //转换为字符
    } finally {
      digestPool.release(container);
    }
  }
}

不知道大家还记不记得本文开始介绍Key时说到的updateDiskCacheKey接口, 实际上在为文件生成String Key时便是先调用这个接口, 然后Key的内部会运用标识的信息生成摘要, 终究再将摘要ByteArray转换为字符串以此来作为文件的标识.

总结

目前Glide 缓存介绍到此结束了. 谢谢大家观看 创作不易, 如有问题感谢指出.