Glide 缓存机制
对于Glide的来说,强大的缓存机制一直是其受欢迎的重要前提,那么Glide缓存机制具体是什么呢,而其实现的原理又是什么呢?这篇文章将在源码层面对Glide缓存机制进行剖析,对Glide有更深层次的了解,那么就直接步入主题吧!
本文章基于Glide 4.11.0 版本
缓存机制
谈到缓存机制,我们常常会想到在Android中的一个缓存流程,也就是三级缓存:
- 内存缓存:优先加载,速度最快
- 本地缓存:次优先加载,速度快
- 网络缓存:最后加载,速度慢,需要网络
正常情况下,我们都会对于图片,会先通过网络将图片下载到本地,然后加载到内存当中。最开始我们当然会先去内存缓存中读取图片,当内存中没有图片的时候,我们再去本地读取,只有当本地也没有图片的时候才会重新向网络进行请求再次加载图片。
Glide也很自然的沿用这一套机制。不过如果将网络缓存不看作Glide内部的缓存机制的话,我们可以如此划分,一种是二级缓存即本地缓存和内存缓存,另一种是三级缓存,也就是在内存缓存中再进行划分为LruCache内存缓存(最近最少缓存策略)和WeakReference内存缓存(弱引用缓存策略),而本地缓存因为只有DiskLruCache硬盘缓存(硬盘缓存策略),所以往往也有人称Glide内部的缓存策略为三级缓存策略。但具体如何划分,各位看官也不必较真,这也就是个不同叫法,我们就不再这上面多花功夫了吧。因为不论是菠萝还是凤梨,它都挺好吃的不是吗?
既然刚刚说到这么三种缓存,我们也从这三种缓存开始去进入Glide的缓存机制的了解流程吧!
下面简要介绍下这三种缓存方式:
- DiskLruCache硬盘缓存: 外部缓存,例如可以将网络下载的图片永久的缓存到手机外部存储中去,并可以将缓存数据取出来使用
- LruCache内存缓存:优先淘汰那些近期最少使用的缓存图片对象。
- WeakReference内存缓存:“弱引用”,即在引用图片对象的同时仍然允许对该对象进行垃圾回收。
LRU (Least Recently Used) 的意思就是近期最少使用算法,它的核心思想就是会优先淘汰那些近期最少使用的缓存对象。
其中内存缓存的主要作用是防止应用重复将图片数据读取到内存当中;而硬盘缓存的主要作用是防止应用重复从网络或其他地方下载和读取数据。
内存缓存
在内存缓存中,Glide首先是默认打开的,我们可以在调用时引用skipMemoryCache进行手动关闭内存缓存这个功能。
Glide.with(this).load("url").skipMemoryCache(false).into(TargetView);
其实在我前面的文章《Glide 源码解读》中对于into方法进行解析时就已经分析到如何构建三级缓存了,这篇文章就不去将如何进入到这个缓存源码的一步步过程都写出来,有兴趣的小伙伴可以自己去这篇文章进行了解。
这里我们就直接进入构造三级缓存的地方,也就是Engine内的loadFromMemory方法,通过此方法可以通过弱引用和Lru方法将缓存读取出来。只要有一个拿到缓存数据就让ImageView去进行加载显示,如果都没有就返回null,进而通过本地和网络拿取数据。当然对于图片的缓存读取速度,也会通过Log日志打印出来。
loadFromMemory #Engine
private EngineResource<?> loadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
}
//通过WeakReference内存缓存拿到缓存数据
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
//通过LruCache内存缓存拿到缓存数据
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
//如果都拿不到就返回null
return null;
}
当然这里需要传入一个值也就是key,key是对每次进行缓存的一个标志,举个例子,如果上次进行缓存的图片大小是400 * 800,那么这次想要取出300 * 400的图片就不能拿到,准确说这里的key就是图片存储的模板,只有存在对应模板的图片才能被获取出来。如下是这次请求拿到的key被影响的条件。
EngineKey key =
keyFactory.buildKey(
model, //图片数据
signature, //签名
width, //宽
height, //高
transformations, //转换方式
resourceClass, //资源类
transcodeClass, //转换类
options); //缓存设置,定义具有可选*默认值的可用组件(解码器,编码器,模型加载器等)选项
loadFromActiveResources #Engine
那么这里我们先从 loadFromActiveResources的获取缓存的方式开始,对缓存进行一个获取。
@Nullable
private EngineResource<?> loadFromActiveResources(Key key) {
EngineResource<?> active = activeResources.get(key);
if (active != null) {
active.acquire();
}
return active;
}
acquire #EngineResource
这里会对图像的引用进行计数,当计数为0时会进行release的对象释放,其中调用release()方法会让变量减1,类似JVM GC中的引用计数器,保证图片在不被引用的时候能被清理,因为在Glide中进行的封装操作,所以也不用担心发现间互相递归引用导致最后无法进行对象的释放操作了。
synchronized void acquire() {
if (isRecycled) {
throw new IllegalStateException("Cannot acquire a recycled resource");
}
++acquired;
}
activeResources #ActiveResources
这里首先会根据activeResources.get(key)的方式去获取,首先看看有没有相关联的实例对象,然后在如何该方法内如果没有获取到会清理所有的弱引用实例对象(ResourceWeakReference)。
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;
}
//清理弱引用资源
@Synthetic
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);
}
//弱引用对象关联
@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.isMemoryCacheable() && isActiveResourceRetentionAllowed
? Preconditions.checkNotNull(referent.getResource())
: null;
isCacheable = referent.isMemoryCacheable();
}
//重置清理
void reset() {
resource = null;
clear();
}
}
至于后续如何通过EngineJob内取出线程进行图片加载这样的操作,我就不进行赘述了,这篇文章的重点在于如何从缓存中获取到数据。
那么接下来,接查看另一个获取图片资源的内存缓存方式–loadFromCache()
loadFromCache #Engine
在LRU缓存方式中,对缓存引用的计数方式和弱引用,如果没有拿到就去弱引用内进行对这个key的启用,然后如果弱引用对象中为空,则进行启用。
private EngineResource<?> loadFromCache(Key key) {
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();//计数
activeResources.activate(key, cached);//启用到弱引用
}
return cached;
}
//从内存中获取资源
private EngineResource<?> getEngineResourceFromCache(Key key) {
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).
//如果已经缓存了EngineResource(典型情况),请保存对象分配
result = (EngineResource<?>) cached;
} else {
result =
new EngineResource<>(
cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
}
return result;
}
remove #MemoryCacheAdapter
其中remove()方法在Lru策略中是移除给定键处的项,如果存在则返回移除项,否则返回null。这里进行了简单的覆写,如果存在则返回通过建查到的值,否则通过赋值为null进行对对象的释放(无引用对象会被GC标记然后回收)
//删除给定键的值,如果存在则返回它,否则返回null。
public Resource<?> remove(@NonNull Key key) {
return null;
}
下面是LRU策略内调用的remove #LruCache方法。
public synchronized Y remove(@NonNull T key) {
final Y value = cache.remove(key);
if (value != null) {
currentSize -= getSize(value);
}
return value;
}
从弱引用取缓存,拿到的话,引用计数+1;从LruCache中拿缓存,拿到的话,引用计数也是+1,同时把LruCache缓存转移到弱应用缓存池中;从EngineJob去加载图片,拿到的话,引用计数也是+1,会把图片放到弱引用。反过来,一旦没有地方正在使用这个资源,就会将其从弱引用中转移到LruCache缓存池中。这也说明了正在使用中的图片使用弱引用来进行缓存,暂时不用的图片使用LruCache来进行缓存的功能。
综上,可以知道在Glide内存缓存有以下几方面步骤:
- 读:是先从lruCache取,取不到再从弱引用中取;
- 存:内存缓存取不到,从网络拉取回来先放在弱引用里,渲染图片,图片对象Resources使用计数加一;
- 渲染完图片,图片对象Resources使用计数减一,如果计数为0,图片缓存从弱引用中删除,放入lruCache缓存。
简单的来说就是从弱引用删除图片缓存,是否支持缓存,缓存到LruCache缓存,不支持缓存直接调用垃圾回收,回收图片
磁盘缓存
说完内存缓存,自然要讲讲磁盘缓存,回到我们之前进行loadFromMemory也就是从内存中获取数据的地方,如果在此处没有拿到数据,那么就可能需要进行启动新的Job去磁盘里看看有没有我们Glide所需要的缓存了。
synchronized (this) {
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
if (memoryResource == null) {
return waitForExistingOrStartNewJob(
glideContext,
model,
signature,
width,
height,
resourceClass,
transcodeClass,
priority,
diskCacheStrategy,
transformations,
isTransformationRequired,
isScaleOnlyOrNoTransform,
options,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache,
cb,
callbackExecutor,
key,
startTime);
}
}
private <R> LoadStatus waitForExistingOrStartNewJob(
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,
EngineKey key,
long startTime) {
EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
if (current != null) {
current.addCallback(cb, callbackExecutor);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Added to existing load", startTime, key);
}
return new LoadStatus(cb, current);
}
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); //拿到磁盘策略的build方式
jobs.put(key, engineJob);
engineJob.addCallback(cb, callbackExecutor);
engineJob.start(decodeJob); //这里开始启动线程
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}
在EngineJob里启用新的线程,在线程中调用start方法进行启用,很明显能发现这里的线程启用器会对三种策略进行判断然后开启。
start #EngineJob
在这里因为无法通过内存获取缓存,然后就需要走Diskcache线程的方式去获取,而这里的线程其实在我们最开始执行Glide的build方法的时候其实已经讲磁盘缓存策略的线程器布置上去了。
public synchronized void start(DecodeJob<R> decodeJob) {
this.decodeJob = decodeJob;
GlideExecutor executor =
decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();
//如果此作业将尝试从磁盘缓存中解码资源,则返回true;
executor.execute(decodeJob);//执行线程
//
}
//Glide中将该线程控制器布置上去
Glide build(@NonNull Context context) {
if (diskCacheExecutor == null) {
diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();
}
decodeFromRetrievedData #DecodeJob
通过执行线程里的run()方法,最终会调用到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();
}
}
private <Data> Resource<R> decodeFromData(
DataFetcher<?> fetcher, Data data, DataSource dataSource) throws GlideException {
try {
if (data == null) {
return null;
}
long startTime = LogTime.getLogTime();
Resource<R> result = decodeFromFetcher(data, dataSource);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded result " + result, startTime);
}
return result;
} finally {
fetcher.cleanup();
}
}
@SuppressWarnings("unchecked")
private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource)
throws GlideException {
LoadPath<Data, ?, R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());
return runLoadPath(data, dataSource, path);
}
LoadPath #LoadPath
首先是loadPath的方式进行查找数据是否存在,但无论是否构建出数据,都将这次搜索进行记录,以便后续再次进行磁盘缓存的搜索。
@Nullable
public <Data, TResource, Transcode> LoadPath<Data, TResource, Transcode> getLoadPath(
@NonNull Class<Data> dataClass,
@NonNull Class<TResource> resourceClass,
@NonNull Class<Transcode> transcodeClass) {
LoadPath<Data, TResource, Transcode> result =
loadPathCache.get(dataClass, resourceClass, transcodeClass);//获取结果
if (loadPathCache.isEmptyLoadPath(result)) {
return null;
} else if (result == null) {
List<DecodePath<Data, TResource, Transcode>> decodePaths =
getDecodePaths(dataClass, resourceClass, transcodeClass);
// It's possible there is no way to decode or transcode to the desired types from a given
// data class.
if (decodePaths.isEmpty()) {
result = null;
} else {
result =
new LoadPath<>(
dataClass, resourceClass, transcodeClass, decodePaths, throwableListPool);
}
loadPathCache.put(dataClass, resourceClass, transcodeClass, result);//记录缓存搜索
}
return result;
}
//从cache中根据key获取之前存储的缓存path
public <Data, TResource, Transcode> LoadPath<Data, TResource, Transcode> get(
Class<Data> dataClass, Class<TResource> resourceClass, Class<Transcode> transcodeClass) {
MultiClassKey key = getKey(dataClass, resourceClass, transcodeClass);
LoadPath<?, ?, ?> result;
synchronized (cache) {
result = cache.get(key);
}
keyRef.set(key);
return (LoadPath<Data, TResource, Transcode>) result;
}
load #LoadPath
之所以采用loadpath的方式,是便于后续可以直接将该拿到的path形式进行直接进行load,而不用回调到嘴上层,加快了磁盘缓存的查询速度。
public Resource<Transcode> load(
DataRewinder<Data> rewinder,
@NonNull Options options,
int width,
int height,
DecodePath.DecodeCallback<ResourceType> decodeCallback)
throws GlideException {
List<Throwable> throwables = Preconditions.checkNotNull(listPool.acquire());
try {
return loadWithExceptionList(rewinder, options, width, height, decodeCallback, throwables);
} finally {
listPool.release(throwables);
}
到此一个磁盘缓存的过程就结束了,其实真正常用的缓存调用还是下面的方式:
磁盘缓存通过diskCacheStrategy方法设置,策略有5种:
- DiskCacheStrategy.ALL 缓存原始数据和处理后的数据,即DATA和RESOURCE的结合
- DiskCacheStrategy.NONE 不缓存任何数据 DiskCacheStrategy.DATA 缓存获取到的原始未解码数据
- DiskCacheStrategy.RESOURCE 缓存解码后的数据
- DiskCacheStrategy.AUTOMATIC 默认选项,自动判断策略
当然如果没有找到该path进行load的话,就会再去寻找原图,或者进行网络加载。
大致流程如下:
- 读:先找处理后(result)的图片,没有的话再找原图。
- 存:先存原图,再存处理后的图。
Glide的缓存机制就到此了,学习其实一直都在帮你,永不止步吧!