本文将从源码角度,学习Glide的缓存机制。
如无特殊说明,源码版本均指:4.6.1
Glide的强大,体现在很多方面,其实缓存是一个很重要部分。Glide的缓存,分为:
(1)内存缓存:基于LruCache和弱引用机制
(2)磁盘缓存:基于DiskLruCache进行封装
Glide的缓存策略,为:
内存缓存–>磁盘缓存–>网络加载
大致流程如下:假设同时开启了内存缓存和磁盘缓存,当程序请求获取图片时,首先从内存中获取,如果内存没有就从磁盘中获取,如果磁盘中也没有,那就从网络上获取这张图片。当程序第一次从网络加载图片后,就将图片缓存到内存和磁盘上。
这两个缓存模块的作用各不相同,内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,而硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。
内存缓存和硬盘缓存的相互结合才构成了Glide极佳的图片缓存效果。下面,将从源码角度,详细进行学习:
缓存key是实现内存和磁盘缓存的唯一标识
重写equals和hashCode方法,来确保只有key对象的唯一性
生成缓存key的地方,在Engine的load方法中
# Engine.class
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();
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
....
return new LoadStatus(cb, engineJob);
}
这里会调用keyFactory的buildkey方法来创建EngineKey对象,这个EngineKey也就是Glide中的缓存Key了。
可见,决定缓存Key的条件非常多,即使你只是改变了一下图片的width或者height,也会生成一个完全不同的缓存Key。
# EngineKeyFactory.class
class EngineKeyFactory {
@SuppressWarnings("rawtypes")
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
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;
}
@Override
public String toString() {
return "EngineKey{"
+ "model=" + model
+ ", width=" + width
+ ", height=" + height
+ ", resourceClass=" + resourceClass
+ ", transcodeClass=" + transcodeClass
+ ", signature=" + signature
+ ", hashCode=" + hashCode
+ ", transformations=" + transformations
+ ", options=" + options
+ '}';
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
throw new UnsupportedOperationException();
}
}
你会发现在EngineKey中重写equals和hashcode方法,这样就能确保只有传入EngineKey的所有参数都相同的情况下才认为是同一个EngineKey对象。
防止应用重复将图片数据读取到内存
缓存原理:弱引用机制和LruCache算法
弱引用机制:当JVM进行垃圾回收时,无论当前的内存是否足够,都会回收掉弱引用关联的对象
LruCache算法:内部采用LinkedHashMap以强引用的方式存储外界的缓存对象,当缓存满时,LruCache会移除较早使用的缓存对象,然后添加新的缓存对象
Glide默认是开启内存缓存功能的。不需要我们额外配置。
那么既然已经默认开启了这个功能,还有什么可讲的用法呢?只有一点,如果你有什么特殊的原因需要禁用内存缓存功能,Glide对此提供了接口:
Glide.with(this)
.load(url)
.apply(RequestOptions.skipMemoryCacheOf(true)) //控制内存缓存是否开启
.into(imageView);
第3行代码中,传入true表示关闭内存缓存。 不写或传入false表示开启内存缓存。
另外,说一下,在Glide3.X 中,写法有些不同:
Glide.with(this)
.load(url)
.skipMemoryCache(true)
.into(imageView);
在前面我们提到了两种类型的内存缓存,那么Glide是如何协调两者的呢?让我们继续回到Engine的load方法:
# Engine.class
public <R> LoadStatus load(...) {
....
//创建EngineKey对象
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
//检查内存的弱引用缓存是否有目标图片
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable); --->2.4.1部分分析
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;
}
//检查内存的Lrucache缓存是否有目标图片
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable); --->2.4.2部分分析
if (cached != null) {
cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
.....
}
在上面的方法中我们可以发现首先根据宽高,图片地址等生成key,然后根据key首先调用loadFromActiveResources获取内存的弱引用缓存的图片,如果获取不到弱引用缓存的图片,才调用loadFromCache获取内存的LruCache缓存。
我们先来看看Glide对弱引用缓存的操作。
从上面Engine的load方法中,我们知道获取弱引用缓存会调用Engine的loadFromActiveResources方法
# Engine.class
@Nullable
private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
//这里的值,即Api中的 RequestOptions.skipMemoryCacheOf(true)
return null;
}
//重点关注get方法,从弱引用中拿数据
EngineResource<?> active = activeResources.get(key);
if (active != null) {
//将EngineResource的引用计数加1
active.acquire();
}
return active;
}
首先就判断了isMemoryCacheable是不是false,如果是false的话就直接返回null。这是什么意思呢?其实很简单,我们刚刚不是学了一个RequestOptions.skipMemoryCacheOf( )方法吗?如果在这个方法中传入true,那么这里的isMemoryCacheable取反 就会是false,表示内存缓存已被禁用。
在第10行代码中,首先会调用ActiveResources的get方法获取到图片资源,然后将EngineResource的引用计数加一,因为此时EngineResource指向了弱引用缓存的图片资源。
# ActiveResources.class
final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
@Nullable
EngineResource<?> get(Key key) {
//从弱引用HashMap中取出对应的弱引用对象
ResourceWeakReference activeRef = activeEngineResources.get(key);
//如果弱引用中关联的EngineResource对象不存在(即EngineResourse被回收)
if (activeRef == null) {
return null;
}
EngineResource<?> active = activeRef.get();
if (active == null) {
//清理弱引用缓存,恢复EngineResource,并保存到LruCache缓存中
cleanupActiveReference(activeRef);
}
return active;
}
在ActiveResources中的get中,会通过activeEngineResources的get方法得到了数据的弱引用对象,而这个activeEngineResources其实就是个HashMap,所以可以根据key值来找到这个弱引用对象。而我们要找的图片资源其实就是这个弱引用对象关联的对象,让我们来看看ResourceWeakReference(第3行代码)的实现:
# ActiveResources.class
@VisibleForTesting
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();
}
void reset() {
resource = null;
clear();
}
}
可以发现这个类其实继承了WeakReference,所以当gc发生时,会回收掉ResourceWeakReference对象关联的EngineResource对象,这个对象封装了我们所要获取的图片资源。
另外这个类还保存了图片资源和图片资源的缓存key,这是为了当关联的EngineResource对象被回收时,可以利用保存的图片资源来恢复EngineResource对象,然后保存到LruCache缓存中并根据key值从HashMap中删除掉关联了被回收的EngineResource对象的弱引用对象。
可以看下EngineResourse被回收的情况:
# ActiveResources.class
@SuppressWarnings("WeakerAccess")
@Synthetic void cleanupActiveReference(@NonNull ResourceWeakReference ref) {
Util.assertMainThread();
//将该弱引用缓存从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.class
@Override
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
Util.assertMainThread();
//删除弱引用缓存
activeResources.deactivate(cacheKey);
//如果开启了内存缓存
if (resource.isCacheable()) {
//将弱引用缓存的数据缓存到LruCache缓存中
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
这样当弱引用缓存所关联的图片资源被回收时,会将图片资源保存到LruCache缓存中,从而保证了当获取不到弱引用缓存的图片时,可以获取的到该图片的LruCache缓存。
弱引用缓存的存储体现在了两个地方:
(1)在主线程展示图片前
(2)获取LruCache缓存时
下面将对这两个地方分别进行学习。
在讲弱引用缓存的存储前,我们首先要明白这个弱引用缓存保存到图片资源到底是图片的原始数据(图片输入流)还是转换后的图片资源,搞明白的话,找到弱引用存储的地方就不是问题了。这里我就不再细讲如何搞明白这个问题,其中一个思路就是从Engine的load方法中获取到弱引用缓存的操作入手,即回调入手。
#Engine.class
public <R> LoadStatus load(....) {
.....
//检查内存弱引用缓存是否有目标图片
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
//此时回调的是SingleRequest的onResourceReady方法
cb.onResourceReady(active, DataSource.MEMORY_CACHE);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
.....
}
这个方法你追踪下去你就会发现其实最后就是展示这个图片资源,因此我们可以确定这个图片资源应该就是转换后的图片,所以存储弱引用缓存应该是在转换图片后的操作。最后我们会发现在EngineJob的handleResultOnMainThread方法中找到了弱引用缓存入口。
代码片3
# EngineJob.class
@Synthetic
void handleResultOnMainThread() {
.....
// 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(this, key, engineResource);
//noinspection ForLoopReplaceableByForEach to improve perf
for (int i = 0, size = cbs.size(); i < size; i++) {
ResourceCallback cb = cbs.get(i);
if (!isInIgnoredCallbacks(cb)) {
engineResource.acquire();
cb.onResourceReady(engineResource, dataSource);
}
}
// Our request is complete, so we can release the resource.
//通知上层删除弱引用缓存数据
engineResource.release();
release(false /*isRemovedFromQueue*/);
}
没错其入口就是我们在Glide图片加载流程提到过的回到主线程展示照片代码的前面,即回调了Engine的onEngineJobComplete来存储弱引用缓存。
另外第17行代码是对弱引用缓存的删除,后面会详细讲解
# Engine.class
@SuppressWarnings("unchecked")
@Override
public void onEngineJobComplete(EngineJob<?> engineJob, 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.activate(key, resource);
}
}
jobs.removeIfCurrent(key, engineJob);
}
# ActiveResources.class
void activate(Key key, EngineResource<?> resource) {
//构建弱引用对象
ResourceWeakReference toPut =
new ResourceWeakReference(
key,
resource,
getReferenceQueue(),
isActiveResourceRetentionAllowed);
//将获取到的缓存图片存储到弱引用对象的HashMap中
ResourceWeakReference removed = activeEngineResources.put(key, toPut);
//key值不重复返回null,key值重复返回旧对象
if (removed != null) {
//如果key值重复,就将之前的弱引用对象的图片资源置为null
removed.reset();
}
}
从这里也可以得到一个结论:正在使用的图片会存储到弱引用缓存中而不是LruCache缓存。
由于这个操作同时也涉及了LruCache的获取,故可以直接看下面对LruCache获取的解析
弱引用缓存的删除其实体现在两处:
JVM进行GC时
弱引用缓存对象引用计数为0时
当JVM进行GC时,由于弱引用对象的特性,导致了弱引用缓存所关联的对象也会被回收,然后就会删除掉这个弱引用缓存对象,这部分我们在弱引用缓存获取的时候也分析过,这里不再进行解析。
在代码片3第17行代码提到,engineResource.release() 方法是通知上层删除弱引用缓存数据。
# EngineResource.class
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");
}
//每次调用release内部引用计数法减一,当为0时,代表没引用,通知上层回收
if (--acquired == 0) {
//回调,listener为Engine类型
listener.onResourceReleased(key, this);
}
}
在这里使用了著名的判断对象是否存活的算法-引用计数法,每次调用EngineResource对象的release方法,都会令该引用减1,当引用计数为0时,表示已经不再使用该对象,即图片不再使用时,就会回调Engine的onResourceReleased方法
# Engine.class
@Override
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
Util.assertMainThread();
activeResources.deactivate(cacheKey);
if (resource.isCacheable()) {
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
# Engine.class
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来进行缓存。
上文我们提到,获取内存缓存时,如果获取不到弱引用缓存时才会调用loadFromCache获取LruCache缓存。让我们看看Engine的loadFromCache方法:
# Engine.class
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存储到弱引用缓存我们已经在上面弱引用缓存的存储中分析过了。
# Engine.class
/**
* 作用:获取图片缓存
* 过程:根据缓存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算法回收掉。
当弱引用缓存删除时,会将缓存存储到LruCache缓存中。(分析可以看弱引用缓存删除操作)
当获取LruCache缓存的同时对该LruCache缓存进行删除操作。(分析可以看LruCache缓存的获取操作)
分析完内存缓存,你会发现弱引用缓存和LruCache缓存真的是环环相扣,密不可分,很多操作都是有关联性的。其流程图如下:
流程图的前提:开启内存缓存,关闭磁盘缓存
防止应用重复的从网络或从其它地方下载和读取数据
使用Glide自定义的DiskLruCache算法
DiskLruCache算法是基于LruCache算法,该算法的应用场景是存储设备的缓存,即磁盘缓存。
磁盘缓存也是默认开启的,默认情况下磁盘缓存的类型为DiskCacheStrategy.AUTOMATIC,当然可以通过代码关闭或者选择其它类型的磁盘缓存:
Glide.with(MainActivity.this)
.load(url)
.apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.RESOURCE)) //设置硬盘储存方式
.into(mImageView);
和 内存缓存一样,关于硬盘缓存的Api,Glide3.x与Glide 4.x有一些不同。
Glide 3.x 的方式如下:
Glide.with(this)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(imageView);
调用diskCacheStrategy()方法并传入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬盘缓存功能了。
这个diskCacheStrategy()方法基本上就是Glide硬盘缓存功能的一切,它可以接收四种参数:
上面四种参数的解释本身并没有什么难理解的地方,但是有一个概念大家需要了解,就是当我们使用Glide去加载一张图片的时候,Glide默认并不会将原始图片展示出来,而是会对图片进行压缩和转换(我们会在后面学习这方面的内容)。总之就是经过种种一系列操作之后得到的图片,就叫转换过后的图片。而Glide默认情况下在硬盘缓存的就是转换过后的图片,我们通过调用diskCacheStrategy()方法则可以改变这一默认行为。
由于原始图片的缓存也属于磁盘缓存,故跟RESOURCE缓存一样删除不仅仅由代码控制,常见删除方式如下:
(1) 用户主动删除手机上的对应文件
(2) 卸载软件
(3) 手动调用Glide.get(applicationContext).clearDiskCache()
从上面的分析可以发现Glide中首先会读取转换后的图片的缓存,然后再读取原始图片的缓存。但是存储的时候恰恰相反,首先存储的是原始图片的缓存,再存储转换后的图片,不过获取和存储都受到Glide使用API的设置的影响。其流程图如下:
流程图的前提:关闭内存缓存或获取不到内存缓存,开启磁盘缓存
推荐文章: