Glide缓存机制
Glide缓存机制
Glide缓存机制简介
- Glide的缓存分为两种,一种是内存缓存,一种是硬盘缓存,其中内存缓存包含弱引用缓存HashMap和LruCache,硬盘缓存就是DiskLruCache。获取缓存的顺序是先去弱引用缓存中寻找,没有再去LruCache中寻找,内存缓存中没有则取硬盘缓存中寻找,如果内存缓存和硬盘缓存中都没有该图片,则再通过HttpUrlConnection新建网络请求去获取图片资源。
内存缓存的取资源
在上一篇博客的介绍中,我们知道Glide请求流程的最后只通过调用Engine的load方法来真正开始加载的逻辑,所以自然而然读缓存的操作也是从Engine的load方法中开始:
public synchronized <R> LoadStatus load(......){
// 获取资源的 key ,参数有8个
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
EngineResource<?> memoryResource;
synchronized (this) {
//去内存中获取
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime)
}
// 如果都获取不到,则去硬盘缓存或网络加载
....
}
}
由以上几行代码,我们把内存缓存的步骤分成以下几步来解析
第一步:获取缓存key
// 获取资源的 key ,参数有8个
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
我们看到,在创建key的时候,会传入8个参数,所以及时我们每次要获取的是同一张图片,但是大小不一样的话,也会生成一个新的缓存key。去进行缓存。
-
Glide是默认开启内存缓存的,如果我们想关掉内存缓存,可以调用:
-
//关闭内存缓存 skipMemoryCache(false)
第二步:从弱引用缓存中获取
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
调用了loadFromActiveResources()方法。
loadFromActiveResources()
@Nullable
private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
//去弱引用activeResources这个HashMap中获取
EngineResource<?> active = activeResources.get(key);
//如果能拿到资源,调用acquire()方法令计数器 +1
if (active != null) {
active.acquire();
}
return active;
}
@Nullable
synchronized EngineResource<?> get(Key key) {
ResourceWeakReference activeRef = activeEngineResources.get(key);
if (activeRef == null) {
return null;
}
EngineResource<?> active = activeRef.get();
//如果没有查询到此弱引用对象,则删除此key
if (active == null) {
cleanupActiveReference(activeRef);
}
return active;
}
synchronized void acquire() {
if (isRecycled) {
throw new IllegalStateException("Cannot acquire a recycled resource");
}
++acquired;
}
这个方法就是去弱引用的activeResources这个HashMap中通过key拿到弱引用的对象,如果获取不到,则队形可能被GC,就从map中移除这个key;如果获取到则调用acquire()方法,令计数器acquire加1,代表被使用,关于计数器的应用,我们最后会总结。
第三步:从内存LruCache中获取
// 从 lrucache 获取对象
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
调用了loadFromCache方法
loadFromCache()
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
//根据key去内存中获取资源
EngineResource<?> cached = getEngineResourceFromCache(key);
//如果获取到了资源,令其计数器加1,并且存放到弱引用缓存中
if (cached != null) {
cached.acquire();
activeResources.activate(key, cached);
}
return cached;
}
getEngineResourceFromCache()
private EngineResource<?> getEngineResourceFromCache(Key key) {
//调用的remove方法去获取,也就是说,获取到资源之后,会把此资源从LruCache中移除
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).
result = (EngineResource<?>) cached;
} else {
result =
new EngineResource<>(
cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
}
return result;
}
- 通过以上两个方法,我们得知从LruCache中获取资源,是首先通过remove方法获取资源,获取到之后将资源从LruCache中移除,令其计数器加1并将此资源存入弱缓存中。
至此,我们就过了一遍内存缓存的拿取资源的流程:先从弱引用中取对象,如果存在,引用计数+1,如果不存在,从 LruCache 取,如果存在,则引用计数+1,并把它存到弱引用中,且自身从 LruCache 移除。
内存缓存的存资源
存储资源也一定是在图片加载的方法Engine的load方法中进行存储。我们看一下:
Engine.load()
public synchronized <R> LoadStatus load(
..
// 获取 key
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
// 从 弱引用中获取对象
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
...
// 从 LruCache 获取对象
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
...
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);
...
}
这里会创建两个关键的对象,一个是EngineJob,这是一个线程池,维护了编码、资源解析、网络下载等工作;另一个是DecodeJob,用来对图片进行解码,它继承了Runnable,相当于EngineJob的一个任务。然后调用engineJob.start(decodeJob),具体细节先不说,等方法结束过后,会回调onResourceReady()方法
onResourceReady()
@Override
public void onResourceReady(Resource<R> resource, DataSource dataSource) {
synchronized (this) {
this.resource = resource;
this.dataSource = dataSource;
}
notifyCallbacksOfResult();
}
notifyCallbacksOfResult()
@Synthetic
void notifyCallbacksOfResult() {
ResourceCallbacksAndExecutors copy;
Key localKey;
EngineResource<?> localResource;
synchronized (this) {
stateVerifier.throwIfRecycled();
//是否被取消
if (isCancelled) {
resource.recycle();
release();
return;
} else if (cbs.isEmpty()) {
throw new IllegalStateException("Received a resource without any callbacks to notify");
} else if (hasResource) {
throw new IllegalStateException("Already have resource");
}
engineResource = engineResourceFactory.build(resource, isCacheable);
hasResource = true;
copy = cbs.copy();
// 引用计数 +1
incrementPendingCallbacks(copy.size() + 1);
localKey = key;
localResource = engineResource;
}
// 把对象put到弱引用上
listener.onEngineJobComplete(this, localKey, localResource);
// 遍历所有图片
for (final ResourceCallbackAndExecutor entry : copy) {
// 把资源加载到 imageview 中,引用计数 +1
entry.executor.execute(new CallResourceReady(entry.cb));
}
//引用计数 -1
decrementPendingCallbacks();
}
从上面看,notifyCallbacksOfResult() 方法做了以下事情
- 图片的引用计数 +1
- 通过 listener.onEngineJobComplete() ,它的回调为 Engine.onEngineJobComplete(),把资源 put 到 弱引用上
- 遍历加载的图片,如果加载成功,则引用计数+1,且通过 cb.onResourceReady(engineResource, dataSource) 回调给 target (imageview) 去加载
- 通过 decrementPendingCallbacks() 释放资源,引用计数 -1
当引用计数减到 0 时,即图片已经没有使用时,就会调用 onResourceReleased() 接口
onResourceReleased()
@Override
public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
// 从弱引用中移除
activeResources.deactivate(cacheKey);
if (resource.isCacheable()) {
//添加到 LruCache 中
cache.put(cacheKey, resource);
} else {
// 回收资源
resourceRecycler.recycle(resource);
}
}
磁盘缓存
如果在内存缓存中拿不到图片,此时Glide就会从两个方面来进行判断:
- 一个是资源类型:该图片是否之前曾被解码、转换并写入过磁盘缓存。
- 一个是数据来源:构建这个图片的资源是否之前被写入过文件缓存。
我们的硬盘缓存;Glide 的硬盘策略可以分为如下几种:
- DiskCacheStrategy.RESOURCE :只缓存解码过的图片
- DiskCacheStrategy.DATA :只缓存原始图片
- DiskCacheStrategy.ALL : 即缓存原始图片,也缓存解码过的图片啊, 对于远程图片,缓存 DATA 和 RESOURCE;对本地使用 只缓存 RESOURCE。
- DiskCacheStrategy.NONE :不使用硬盘缓存
- DiskCacheStrategy.AUTOMATIC :默认策略,会对本地和和远程图片使用最佳的策略;对下载网络图片,使用 DATA,对于本地图片,使用 RESOURCE
在讲述磁盘缓存的源码时,先附一张流程图:
从上面我们知道,一个图片的加载在 DecodeJob 这个类中,这个任务由 EngineJob 这个线程池去执行的。去到 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);
}
刚开始 runReason 初始化为 INITIALIZE , 所以它会走第一个 case。
private Stage getNextStage(Stage current) {
switch (current) {
case INITIALIZE:
return diskCacheStrategy.decodeCachedResource()
? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE);
case RESOURCE_CACHE:
return diskCacheStrategy.decodeCachedData()
? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE);
case DATA_CACHE:
// Skip loading from source if the user opted to only retrieve the resource from cache.
return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
case SOURCE:
case FINISHED:
return Stage.FINISHED;
default:
throw new IllegalArgumentException("Unrecognized stage: " + current);
}
}
getNextStage 其实就是对当前的缓存策略进行判断,由于我们的策略为DiskCacheStrategy.ALL ,所以 diskCacheStrategy.decodeCachedResource() 永远为true,即会解析解码的流程即去尝试拿磁盘中解码过的图片资源,所以 State 被赋值为 Stage.RESOURCE_CACHE(State是一个标记)。接着回到runWrapped()方法中,继续执行调用 currentGenerator = getNextGenerator() 拿到当前的解码器为 ResourceCacheGenerator,然后调用runGenerators()方法,这个方法是关键:
runGenerators()
private void runGenerators () {
currentThread = Thread.currentThread( );
startFetchTime = LogTime.getLogTime( );
boolean isstarted = false;
while ( !iscancelled && currentGenerator != null && ! (isstarted = currentGenerator. startNext())){
stage = getNextstage(stage);
currentGenerator = getNextGenerator();
if (stage == stage.SOURCE) {
reschedule();
return;
}
if ((stage == stage.FINISHED || iscancelled) && !isstarted){
notifyFailed();
}
}
这里面维护了一个while循环,不断的通过 调用当前解码器的startNext() 方法取尝试从DiskLruCache中拿到对象,如果拿不到,则通过while循环通过getNextstage(stage)和getNextGenerator()方法拿到下一个解码器再次寻找,当stage == Stage.SOURCE 的时候,会调用reschedule()重新调用DecodeJob的run方法重新进行RunWrapper方法。
startNext()
@Override
public boolean startNext() {
List<Key> sourceIds = helper.getCacheKeys();
...
while (modelLoaders == null || !hasNextModelLoader()) {
resourceClassIndex++;
if (resourceClassIndex >= resourceClasses.size()) {
sourceIdIndex++;
// 由于没有缓存,最后会在这里退出这个循环
if (sourceIdIndex >= sourceIds.size()) {
return false;
}
resourceClassIndex = 0;
}
Key sourceId = sourceIds.get(sourceIdIndex);
//获取缓存 key
currentKey =
new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoops
helper.getArrayPool(),
sourceId,
helper.getSignature(),
helper.getWidth(),
helper.getHeight(),
transformation,
resourceClass,
helper.getOptions());
// 尝试从 DiskLruCache 拿数据
cacheFile = helper.getDiskCache().get(currentKey);
if (cacheFile != null) {
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;
}
由于第一次肯定是拿不到缓存的,所以 while (modelLoaders == null || !hasNextModelLoader()) 循环会一直运行,直到返回 false。
同理,接着来拿到了DataCacheGenerator之后也是同样,最后,当 stage == Stage.SOURCE 的时候,才会退出,并调用reschedule() 方法。
@Override
public void reschedule() {
runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
callback.reschedule(this);
}
在 reschedule() 方法中,会把 runReason 的状态改成RunReason.SWITCH_TO_SOURCE_SERVICE ,并重新回调 run 方法,又会调用 runWrapped() 方法,但此时的 runReason 已经变成了 SWITCH_TO_SOURCE_SERVICE,所以它会执行 runGenerators() 方法(代码上面有):
此时当前解码器currentGenerator,并不为null,所以此时会调用SourceGenerator的startNext()方法:
@Override
public boolean startNext() {
if (dataToCache != null) {
Object data = dataToCache;
dataToCache = null;
// 存储到 DiskLruCache
cacheData(data);
}
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,第一次肯定会 null,所以,可以先不管。由于还并没有给sourceCacheGenerator赋值,所以它为null,这里可以直接看 loadData.fetcher.loadData(); 这个方法,loadData() 是个接口,它有很多个实现方法,由于我们这里是网络下载,所以去到 HttpUrlFetcher.loadData() 中:
loadData()
@override
public void loadData(@NonNull Priority priority,
@NonNull Datacallback<?super Inputstream> callback){
long startTime = LogTime.getLogTime( );
try {
Inputstream result = loadDatawithRedirects(glideur1.toURL(),0,null,glideur1.getHeaders());
callback.onDataReady(result);
}catch (TOException e) {
if (Log.isLoggable(TAG,Log.DEBUG))
Log.d(TAG,msg: "Failed to load data for ur1", e);
}
}
可以看到,它拿到 inputStream 后,通过 onDataReady() 方法回调回去
@Override
public void onDataReady(Object data) {
DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
dataToCache = data;
// We might be being called back on someone else's thread. Before doing anything, we should
// reschedule to get back onto Glide's thread.
cb.reschedule();
} else {
cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
loadData.fetcher.getDataSource(), originalKey);
}
}
在 SourceGenerator.onDataReady() 中,对 dataToCache 进行赋值;然后又调用 cb.reschedule()方法,从头再来一次,可是这一次dataToCache已经不是null了,所以会执行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());
originalKey = new DatacacheKey(loadData.sourceKey,helper.getsignature());
helper.getDiskcache().put(originalKey,writer);
if (Log.isLoggable( TAG,Log.VERBOSE))
Log.v(TAG,msg: "Finished encoding source to cache"
+ ",key: " + originalKey
+ ", data: " + dataTocache+ ", encoder: " +encoder
+ ", duration: " + LogTime.getELapsedMillis(startTime)) ;
}finally {
loadData.fetcher.cleanup();
}
sourcecacheGenerator =new DataCacheGenerator(collections.singLetonList(loadData.sourceKey), helper,cb: this);
}
可以看到,对这个已经解析完的数据,通过 helper.getDiskCache().put() 方法,存到到 DiskLruCache 硬盘缓存中。并通过 loadData.fetcher.clearup() 清除任务,把sourcecacheGenerator赋值为 DataCacheGenerator。
回到SourceGenerator的startNext方法
@override
public boolean startNext() {
if (dataTocache != null){
object data = dataTocache;dataTocache = null;
cacheData(data);
}
if (sourceCacheGenerator != null && sourcecacheGenerator.startNext()){
return true;
}
sourcecacheGenerator = null;
loadData = null:
}
此时 sourceCacheGenerator 不为 null,所以会走 DataCacheGenerator 的startNext() 方法。
在这之前已经将网络请求获取的图片加载到DiskLruCache中了,所以已经拿到了请求的图片资源,之后就会经过一系列的调用,调用到EngineJob的onResourceReady方法,这个方法我们在内存缓存的时候说过,它会把资源存放到弱引用中并且加载图片。
想必通过上面的一堆代码展示,对于硬盘的缓存的理解还是会模糊,所以我们在下面总结一下。
硬盘缓存小结
总结一下硬盘缓存的步骤:
- 一:根据硬盘策略,选择不同的缓存策略。以下的步骤基于硬盘策略为DiskCacheStrategy.ALL,即缓存解码后的图片,也缓存原始图片。
- 二:先调用DecodeJob的run方法,进而调用runWrapped方法,根据runReason这个标记,生成不同的解码器。
- 三:先获取ResourceCaheGenerator,去检查硬盘DiskLruCache中有没有解码后的图片资源,如果没有则拿到下一个解码器DataCacheGenerator,调用其的startNext方法,去检查硬盘DiskLruCache中有没有原始的图片资源。如果还是没有,则最后调用rescgedule方法再次调用DncodeJob的run方法并且更改runReason的值。
- 四:根据runReason,将SourceGenerator赋值给当前解码器,第一次dataToCache一定为null,所以一定会进行网络请求,去根据URl获取图片资源,然后再回调DecodeJob的run方法
- 五:同样会走到这一步,这一次因为已经进行过网络的获取,所以dataToCache不为null,即会将图片资源放到硬盘缓存DiskLruCache中,然后再把DataCacheGenerator赋值给SourceGenerator,然后调用SourceGenerator.startNext即DataCacheGenerator的startNext方法。
- 六:由于此时可以在硬盘中拿到数据了,所以和第三步的执行过程不一样,即会经过一系列步骤会回调到onResourceReady方法将图片显示,并将资源放到弱引用缓存中。
再次结合下面这个图看会更好理解: