
前言
Glide作为在Android中广泛使用的图片加载库为人所熟悉. 最近在项目中刚好遇到一个与Glide缓存策略相关的问题并且也一直想整理关于源码的分析文章. 所以写下了这篇文章.
本文所用Glide版本为: 4.16.0 阅读本篇前了解以下几点可以帮助更好的理解
- Java 引用类型以及引用队列
- Lru算法原理
- Bitmap基本知识
- MessageDigest摘要算法
Key的生成
Glide中专门定义了Key接口用于标识数据, 其中后两个函数我们都很熟悉用于生成hashCode以及对象比较. 第一个接口可能就比较陌生了. 它主要作用是在磁盘缓存时, 将所有能作为唯一标识的信息生成摘要, 后续在磁盘缓存时会介绍到. 下面先来看一下内存缓存时使用的Key.
java
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来对资源进行查询.
java
// 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加载. 观察第二次加载是否能加载出第二张图片
结果是第二张图片并不会被加载, 显示的仍然是第一张图片
解决方案:
kotlin
val file = File(PATH)
Glide.with(this@EngineKeyActivity)
.load(file)
//增加文件最后修改时间作为signature 前后生成的Key自然不同 不会错误复用图片缓存
.signature(ObjectKey(file.lastModified()))
.into(binding.img)
以上是针对本地图片加载的修改方案. 具体还是要根据业务场景来作修改, 这里还是进行错误分析以及提供思路.
Glide Resource 资源包装类
在了解Glide 缓存实现之前, 我们先来了解一下Glide的资源包装接口. 这与后面介绍Glide缓存机制直接相关
java
/**
* 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的内存缓存息息相关.
java
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内部缓存主要被分为了三个部分
- ActiveResources(活动缓存)
- LruResourceCache(Lru策略缓存)
- DiskCache(磁盘缓存)
其中ActiveResources
与LruResourceCache
都属于内存缓存, 而之所以这样区分是有重要原因的.
假设Glide去掉ActiveResources
而只使用LruResourceCache
的话, 由于Lru的实现是固定缓存数量移除最近最少被使用的缓存, 那么当缓存数量达到满时那么就会有resource被移除. 下面看一下被移除的resource具体会执行什么逻辑
java
@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(活动缓存)
由于代码较长, 所以删去大部分不太重要的代码. 推荐大家可以配合源码对比食用
java
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();
}
}
}
这一部分代码非常长我们一步步来分析
从上面代码可以看出以下几点
ActiveResources
直接缓存EngineResource
类型- 通过Map + WeakReference + ReferenceQueue的方式对缓存进行存储, 开启Thread循环清理已被回收对象
- 通过
activate
deactivate
get
加入 移除 获取缓存
仅通过以上无法看出ActiveResources
是如何与当前缓存是否正在使用相关的, 下面来看一下activate
deactivate
get
的调用路径
java
// 尝试从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算法实现, 并且内部使用了策略模式制定了多种策略, 下面来看一下策略抽象类中定义的接口
java
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
java
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
java
public enum DataSource {
// 本地
LOCAL,
// 服务端
REMOTE,
// 本地原始数据缓存
DATA_DISK_CACHE,
// 本地变换后的数据缓存
RESOURCE_DISK_CACHE,
// 内存缓存
MEMORY_CACHE,
}
具体的DiskCacheStrategy
策略实现类分为以下几种
ALL
: 缓存服务端原始数据以及解码后的数据NONE
: 统统不存DATA
: 仅缓存解码前的原始数据RESOURCE
: 仅缓存解码后的数据AUTOMATIC
: 默认策略 缓存服务端原始数据, 对于本地数据缓存变换后的数据
从以上可以看出, Glide对于磁盘缓存也是做了两种缓存类型进行存储. 分别是原始数据类型以及解码转换后的数据类型.
对于两种缓存类型分别用了两种Key DataCacheKey
和ResourceCacheKey
来进行映射. 对于两种Key就不多介绍了, DataCacheKey
其实是是对当前请求数据的Key以及开发者自定义signature
的包装, ResourceCacheKey
则在前者的基础上增加了解码以及变换相关的属性.
下面来看一下Key是如何映射到本地文件的
java
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;
}
}
java
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 缓存介绍到此结束了. 谢谢大家观看 创作不易, 如有问题感谢指出.