文章目录
- 一、缓存简介
- 二、缓存用法
- 内存缓存方式
- 磁盘缓存方式
- 三、缓存KEY
- 四、内存缓存
- 内存缓存流程
- 五、磁盘缓存
- 磁盘缓存流程
Android Glide图片加载框架系列文章
Android Glide图片加载框架(一)基本用法
Android Glide图片加载框架(二)源码解析之with()
Android Glide图片加载框架(二)源码解析之load()
Android Glide图片加载框架(二)源码解析之into()
Android Glide图片加载框架(三)缓存机制
一、缓存简介
Glide的缓存设计可以说是非常先进的,考虑的场景也很周全。在缓存这一功能上,Glide又将它分成了两个模块,一个是 内存缓存
,一个是 磁盘缓存
。
这两个缓存模块的作用各不相同,
-
内存缓存
的主要作用是防止应用重复将图片数据读取到内存当中; -
磁盘缓存
的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。
内存缓存
和 硬盘缓存
的相互结合才构成了Glide极佳的图片缓存效果,那么接下来我们就分别来分析一下这两种缓存的使用方法以及它们的实现原理。
二、缓存用法
内存缓存方式
RequestOptions options = new RequestOptions();
// 禁止内存缓存
options.skipMemoryCache(true);Glide.with(this).load(url).apply(options).into(imageView);
磁盘缓存方式
RequestOptions options = new RequestOptions();
// 磁盘不缓存
options.diskCacheStrategy(DiskCacheStrategy.NONE);Glide.with(this).load(url).apply(options).into(imageView);
可以设置5种模式:
-
DiskCacheStrategy.NONE:
表示不缓存任何内容 -
DiskCacheStrategy.DATA:
表示只缓存原始图片 -
DiskCacheStrategy.RESOURCE:
表示只缓存转换过后的图片 -
DiskCacheStrategy.ALL:
表示既缓存原始图片,也缓存转换过后的图片 -
DiskCacheStrategy.AUTOMATIC:
表示让Glide根据图片资源智能地选择使用哪一种缓存策略(默认选项)
三、缓存KEY
既然是缓存功能,就必然会有用于进行缓存的Key。那么Glide的缓存Key是怎么生成的呢?我不得不说,Glide的缓存Key生成规则非常繁琐,决定缓存Key的参数竟然有8个之多。不过繁琐归繁琐,至少逻辑还是比较简单的,我们先来看一下Glide缓存Key的生成逻辑。
生成缓存Key的代码在 Engine
类的 load()
方法当中,这部分代码我们在上一篇文章当中已经分析过了,只不过当时忽略了缓存相关的内容,那么我们现在重新来看一下:
public class Engine implements EngineJobListener,MemoryCache.ResourceRemovedListener,EngineResource.ResourceListener {...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 = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,resourceClass, transcodeClass, options);...}...
}
第27行可见,决定缓存Key的条件非常多,即使你用override()方法改变了一下图片的width或者height,也会生成一个完全不同的缓存Key。
缓存key是一个 EngineKey
对象,该类重写了 equals()
和 hashCode()
方法,保证只有传入EngineKey的所有参数都相同的情况下才认为是同一个 EngineKey
对象,代码如下:
class EngineKey implements Key {...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;}@Overridepublic 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;}...
}
四、内存缓存
默认情况下,Glide自动就是开启内存缓存的
。也就是说,当我们使用Glide加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用Glide再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了,这样无疑就可以大幅度提升图片的加载效率。比方说你在一个RecyclerView当中反复上下滑动,RecyclerView中只要是Glide加载过的图片都可以直接从内存当中迅速读取并展示出来,从而大大提升了用户体验。
而Glide最为人性化的是,你甚至不需要编写任何额外的代码就能自动享受到这个极为便利的内存缓存功能,因为Glide默认就已经将它开启了。
那么既然已经默认开启了这个功能,还有什么可讲的用法呢?只有一点,如果你有什么特殊的原因需要禁用内存缓存功能,Glide对此提供了接口:
RequestOptions options = new RequestOptions();
options.skipMemoryCache(true);Glide.with(this).load(url).apply(options).into(img);
可以看到,只需要调用skipMemoryCache()方法并传入true,就表示禁用掉Glide的内存缓存功能
。
接下来就让我们就通过阅读源码来分析一下Glide的内存缓存功能是如何实现的。
内存缓存使用弱引用和LruCache算法结合完成的
,弱引用来缓存的是正在使用中的图片。图片封装类Resources内部有个计数器判断是该图片否正在使用。
内存缓存流程
-
读:
是先从弱引用中取,取不到再从lruCache取; -
存:
内存缓存取不到,从网络拉取回来先放在弱引用里,渲染图片,图片对象Resources使用计数加一; -
渲染完图片:
图片对象Resources使用计数减一,如果计数为0,图片缓存从弱引用中删除,放入lruCache缓存。
上篇提到,Engine
在加载流程的中的入口方法是 load
方法:
public class Engine implements EngineJobListener,MemoryCache.ResourceRemovedListener,EngineResource.ResourceListener {...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 = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;// 生成缓存keyEngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,resourceClass, transcodeClass, options);// 从弱引用获取图片EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);if (active != null) {cb.onResourceReady(active, DataSource.MEMORY_CACHE);if (VERBOSE_IS_LOGGABLE) {logWithTimeAndKey("Loaded resource from active resources", startTime, key);}return null;}// 从LruCache获取缓存图片EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);if (cached != null) {cb.onResourceReady(cached, DataSource.MEMORY_CACHE);if (VERBOSE_IS_LOGGABLE) {logWithTimeAndKey("Loaded resource from cache", startTime, key);}return null;}EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);if (current != null) {current.addCallback(cb);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);jobs.put(key, engineJob);engineJob.addCallback(cb);engineJob.start(decodeJob);if (VERBOSE_IS_LOGGABLE) {logWithTimeAndKey("Started new load", startTime, key);}return new LoadStatus(cb, engineJob);}...
}
上面是从内存缓存中读取图片的主流程:
-
生成缓存的key。
-
从弱引用获取图片。
-
弱引用没取到,在从LruCache获取缓存图片。
-
内存缓存取不到,进入异步处理。
我们具体看取图片的两个方法 loadFromActiveResources()
和 loadFromCache()
。
-
loadFromActiveResources
使用的就是弱引用。 -
loadFromCache
使用的就是LruCache算法。
我们来看一下它们的源码:
public class Engine implements EngineJobListener,MemoryCache.ResourceRemovedListener,EngineResource.ResourceListener {...private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {if (!isMemoryCacheable) {return null;}EngineResource<?> active = activeResources.get(key);if (active != null) {active.acquire();}return active;}private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {if (!isMemoryCacheable) {return null;}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).result = (EngineResource<?>) cached;} else {result = new EngineResource<>(cached, true /*isMemoryCacheable*/, true /*isRecyclable*/);}return result;}...
}
loadFromActiveResources()
方法:
-
首先就判断
isMemoryCacheable
是不是false
,如果是false的话就直接返回null。这就是skipMemoryCache()
方法设置的是否内存缓存已被禁用。 -
然后从
activeResources
当中取值,使用activeResources来缓存正在使用中的图片,用来保护正在使用中的图片不会被LruCache算法回收掉。
loadFromCache()
方法:
-
首先就判断
isMemoryCacheable
是不是false
,如果是false的话就直接返回null。这就是skipMemoryCache()
方法设置的是否内存缓存已被禁用。 -
然后调用
getEngineResourceFromCache()
方法来获取缓存。在这个方法中,会从中获取图片缓存LruResourceCache
,LruResourceCache其实使用的就是LruCache算法实现的缓存。 -
当我们从
LruResourceCache
中获取到缓存图片之后会将它从缓存中移除,将缓存图片存储到activeResources
当中。activeResources就是弱引用的HashMap,用来缓存正在使用中的图片。
这样我们把从内存读取图片缓存的流程搞清了,那是什么时候存储的呢。想想什么时候合适?是不是应该在异步处理获取到图片后,再缓存到内存?
EngineJob
获取到图片后 会回调Engine的 onEngineJobComplete()
。我们来看下做了什么:
public class Engine implements EngineJobListener,MemoryCache.ResourceRemovedListener,EngineResource.ResourceListener {...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);}...
}
在 onEngineJobComplete()
方法里将正在加载的图片放到弱引用缓存。那什么时候放在LruCache里呢?当然是在使用完,那什么时候使用完呢?
那我们来看 EngineResource
这个类是怎么标记自己是否在被使用的。EngineResource是用一个acquired变量用来记录图片被引用的次数,调用acquire()方法会让变量加1,调用release()方法会让变量减1,代码如下所示:
class EngineResource<Z> implements Resource<Z> {...private int acquired;void acquire() {if (isRecycled) {throw new IllegalStateException("Cannot acquire a recycled resource");}if (!Looper.getMainLooper().equals(Looper.myLooper())) {throw new IllegalThreadStateException("Must call acquire on the main thread");}++acquired;}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");}if (--acquired == 0) {listener.onResourceReleased(key, this);}}...
}
可以看出当引用计数acquired变量为0,就是没有在使用了,然后调用了 listener.onResourceReleased(key, this);
。
这个 listener
就是 Engine
对象,我们来看下它的 onResourceReleased()
方法:
public class Engine implements EngineJobListener,MemoryCache.ResourceRemovedListener,EngineResource.ResourceListener {...public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {Util.assertMainThread();activeResources.deactivate(cacheKey);if (resource.isCacheable()) {cache.put(cacheKey, resource);} else {resourceRecycler.recycle(resource);}}...
}
做了三件事:
-
从弱引用删除图片缓存
-
是否支持缓存,缓存到LruCache缓存
-
不支持缓存直接调用垃圾回收,回收图片
到这里内存缓存的读和存的流程就介绍完了,根据源码回头看看我们之前列的Glide内存缓存流程,就清晰很多了。
五、磁盘缓存
磁盘缓存流程
-
读:
先找处理后(result)的图片,没有的话再找原图。 -
存:
先存原图,再存处理后的图。
注:
diskCacheStrategy设置的的缓存模式即影响读取,也影响存储。
在判断了两级内存缓存之后,如果拿不到缓存,就会接着创建 EngineJob
和 DecodeJob
,然后接着就会调用进 DecodeJob
线程的 run()
方法:
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback,Runnable,Comparable<DecodeJob<?>>,Poolable {...@Overridepublic void run() {// This should be much more fine grained, but since Java's thread pool implementation silently// swallows all otherwise fatal exceptions, this will at least make it obvious to developers// that something is failing.GlideTrace.beginSectionFormat("DecodeJob#run(model=%s)", model);// Methods in the try statement can invalidate currentFetcher, so set a local variable here to// ensure that the fetcher is cleaned up either way.DataFetcher<?> localFetcher = currentFetcher;try {if (isCancelled) {notifyFailed();return;}runWrapped();} catch (Throwable t) {// Catch Throwable and not Exception to handle OOMs. Throwables are swallowed by our// usage of .submit() in GlideExecutor so we're not silently hiding crashes by doing this. We// are however ensuring that our callbacks are always notified when a load fails. Without this// notification, uncaught throwables never notify the corresponding callbacks, which can cause// loads to silently hang forever, a case that's especially bad for users using Futures on// background threads.if (Log.isLoggable(TAG, Log.DEBUG)) {Log.d(TAG, "DecodeJob threw unexpectedly"+ ", isCancelled: " + isCancelled+ ", stage: " + stage, t);}// When we're encoding we've already notified our callback and it isn't safe to do so again.if (stage != Stage.ENCODE) {throwables.add(t);notifyFailed();}if (!isCancelled) {throw t;}} finally {// Keeping track of the fetcher here and calling cleanup is excessively paranoid, we call// close in all cases anyway.if (localFetcher != null) {localFetcher.cleanup();}GlideTrace.endSection();}}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);}}...
}
run()
中主要还是调用的 runWrapper()
方法,继而调用 runGenerator()
:
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback,Runnable,Comparable<DecodeJob<?>>,Poolable {...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;}}// We've run out of stages and generators, give up.if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {notifyFailed();}// Otherwise a generator started a new load and we expect to be called back in// onDataFetcherReady.}...
}
这里调用了一个循环获取解析生成器 Generator
的方法,而解析生成器有多个实现类:ResourcesCacheGenerator
、SourceGenerator
、DataCacheGenerator
,它们负责各种硬盘缓存策略下的缓存管理,所以这里关键的条件在于 currentGenerator.startNext()
循环获取每个Generator能否获取到缓存,获取不到就通过 getNextGenerator()
进行下一种:
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback,Runnable,Comparable<DecodeJob<?>>,Poolable {...private DataFetcherGenerator getNextGenerator() {switch (stage) {case RESOURCE_CACHE:return new ResourceCacheGenerator(decodeHelper, this);case DATA_CACHE:return new DataCacheGenerator(decodeHelper, this);case SOURCE:return new SourceGenerator(decodeHelper, this);case FINISHED:return null;default:throw new IllegalStateException("Unrecognized stage: " + stage);}}...
}
所以我们看看 ResourceCacheGenerator.startNext()
,看下它是用什么来缓存的,其中部分代码如下:
class ResourceCacheGenerator implements DataFetcherGenerator,DataFetcher.DataCallback<Object> {...public boolean startNext() {...while (modelLoaders == null || !hasNextModelLoader()) {...Key sourceId = sourceIds.get(sourceIdIndex);Class<?> resourceClass = resourceClasses.get(resourceClassIndex);Transformation<?> transformation = helper.getTransformation(resourceClass);currentKey =new ResourceCacheKey(// NOPMD AvoidInstantiatingObjectsInLoopshelper.getArrayPool(),sourceId,helper.getSignature(),helper.getWidth(),helper.getHeight(),transformation,resourceClass,helper.getOptions());cacheFile = helper.getDiskCache().get(currentKey);if (cacheFile != null) {sourceKey = sourceId;modelLoaders = helper.getModelLoaders(cacheFile);modelLoaderIndex = 0;}}...return started;}...
}
这里通过一个资源的关键信息生成key,然后调用 helper.getDiskCache().get()
,我们跟进去 DiskCache
看看:
final class DecodeHelper<Transcode> {...DiskCache getDiskCache() {return diskCacheProvider.getDiskCache();}...
}
class DecodeJob<R> implements DataFetcherGenerator.FetcherReadyCallback,Runnable,Comparable<DecodeJob<?>>,Poolable {...interface DiskCacheProvider {DiskCache getDiskCache();}...
}
可以看到最终是调用了 DiskCacheProvider
接口的 getDiskCache()
方法获取一个 DiskCache
对象,那么这个D对象又是什么来头呢?
public interface DiskCache {...
}
可以看到这是一个用来缓存硬盘数据的接口,那么它的实现就是我们要找的最终目标:
public class DiskLruCacheWrapper implements DiskCache {...private DiskLruCache diskLruCache;...
}
里面的就不详细分析下去了,这里主要维护了一个 DiskLruCache
,Glide就是通过这个来实现硬盘缓存的。
可以看到Glide的硬盘缓存是依靠DiskLruCache来进行缓存的,同样也是Lru算法。