本文Glide源码基于4.9,版本下载地址如下:Glide 4.9
前言
在分析了Glide的图片加载流程后,更加发觉到Glie的强大,于是这篇文章将继续深入分析Glide的缓存策略。不过今天的文章的源码很多基于上一篇加载流程的基础之上,因此还没有看上一篇的小伙伴,建议先去阅读Glide4.9源码解析-图片加载流程效果会更佳哟!
一、设计
1. 二级缓存
- 内存缓存:基于LruCache和弱引用机制
- 磁盘缓存:基于DiskLruCache进行封装
Glide有几级缓存?对于这个问题,网上的答案不一,有的认为是五级缓存,也有的认为是三级缓存,但我个人认为是二级缓存,因为个人感觉网络加载并不属于缓存(如果有错误,欢迎在评论指出)
2. 缓存策略
内存缓存–>磁盘缓存–>网络加载
Glide的缓存策略大致是这样的:假设同时开启了内存缓存和磁盘缓存,当程序请求获取图片时,首先从内存中获取,如果内存没有就从磁盘中获取,如果磁盘中也没有,那就从网络上获取这张图片。当程序第一次从网络加载图片后,就将图片缓存到内存和磁盘上。
二、流程
1. 生成缓存key
1.1 作用
缓存key是实现内存和磁盘缓存的唯一标识
1.2 原理
重写equals和hashCode方法,来确保只有key对象的唯一性
1.3 源码解析
生成缓存key的地方其实就在于我们上一篇文章提到的过的Engine的load方法中
Engine#load
1 | public synchronized <R> LoadStatus load( |
这里会调用keyFactory的buildkey方法来创建EngineKey对象,并将load传入的数据(String,URL等),图片的宽高,签名,设置参数等传进去。
KeyFactory#buildKey
1 | EngineKey buildKey(Object model, Key signature, int width, int height, |
可以发现KeyFactory的buildKey方法就是简单的返回了一个EngineKey对象。所以我们来看看这个EngineKey
EngineKey
1 | class EngineKey implements Key { |
你会发现在EngineKey中重写equals和hashcode方法,这样就能确保只有传入EngineKey的所有参数都相同的情况下才认为是同一个EngineKey对象。
2. 内存缓存
2.1 作用
防止应用重复将图片数据读取到内存
2.2 原理
缓存原理:弱引用机制和LruCache算法
弱引用机制:当JVM进行垃圾回收时,无论当前的内存是否足够,都会回收掉弱引用关联的对象
LruCache算法:内部采用LinkedHashMap以强引用的方式存储外界的缓存对象,当缓存满时,LruCache会移除较早使用的缓存对象,然后添加新的缓存对象
缓存实现:正在使用的图片使用弱引用机制进行缓存,不在使用中的图片使用LruCache来进行缓存。
2.3 配置
Glide默认情况下是开启了内存缓存的,即你不需要做任何处理,只需要通过下面代码正常调用Glide的三部曲即可。
1 | Glide.with(getContext()).load(url).into(imageView); |
那我们要关闭内存缓存咋整呢?强大的Glide当然思考了这种问题,Glide提供了非常便捷的API,因此我们只需要通过调用来RequestOptions.skipMemoryCacheOf()并传入true,表示跳过内存缓存,即禁用内存缓存。
1 | Glide.with(getContext()) |
2.4 源码解析
在前面我们提到了两种类型的内存缓存,那么Glide是如何协调两者的呢?让我们继续回到Engine的load方法
Engine#load
1 | public synchronized <R> LoadStatus load( |
在上面的方法中我们可以发现首先根据宽高,图片URL地址等生成key,然后根据key首先调用loadFromActiveResources获取内存的弱引用缓存的图片,如果获取不到弱引用缓存的图片,才调用loadFromCache获取内存的LruCache缓存。因此我们先来看看Glide对弱引用缓存的操作。
1. 弱引用缓存
1.1 获取
从上面Engine的load方法中,我们知道获取弱引用缓存会调用Engine的loadFromActiveResources方法
Engine#loadFromActiveResources
1 | private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) { |
这里首先会调用ActiveResources的get方法获取到图片资源,然后将EngineResource的引用计数加一,因为此时EngineResource指向了弱引用缓存的图片资源。
ActiveResources#get
1 | final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>(); |
在ActiveResources中的get中,会通过activeEngineResources的get方法得到了数据的弱引用对象,而这个activeEngineResources其实就是个HashMap,所以可以根据key值来找到这个弱引用对象。而我们要找的图片资源其实就是这个弱引用对象关联的对象,让我们来看看ResourceWeakReference的实现
1 | static final class ResourceWeakReference extends WeakReference<EngineResource<?>> { |
可以发现这个类其实继承了WeakReference,所以当gc发生时,会回收掉ResourceWeakReference对象关联的EngineResource对象,这个对象封装了我们所要获取的图片资源。另外这个类还保存了图片资源和图片资源的缓存key,这是为了当关联的EngineResource对象被回收时,可以利用保存的图片资源来恢复EngineResource对象,然后保存到LruCache缓存中并根据key值从HashMap中删除掉关联了被回收的EngineResource对象的弱引用对象。可以看下EngineResourse被回收的情况
ActiveResources#cleanupActiveReference
1 | void cleanupActiveReference(@NonNull ResourceWeakReference ref) { |
Engine#onResourceReleased
1 | public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) { |
这样当弱引用缓存所关联的图片资源被回收时,会将图片资源保存到LruCache缓存中,从而保证了当获取不到弱引用缓存的图片时,可以获取的到该图片的LruCache缓存。
1.2 存储
弱引用缓存的存储体现在了两个地方:
- 在主线程展示图片前
- 获取LruCache缓存时
下面将对这两个地方分别进行解剖!
在主线程展示图片前存储
在讲弱引用缓存的存储前,我们首先要明白这个弱引用缓存保存到图片资源到底是图片的原始数据(图片输入流)还是转换后的图片资源,搞明白的话,找到弱引用存储的地方就不是问题了。这里我就不再细讲如何搞明白这个问题,其中一个思路就是从Engine的load方法中获取到弱引用缓存的操作入手,即回调入手。
1 | //检查内存弱引用缓存是否有目标图片 |
这个方法其实在上篇文章图片加载流程也提到过,然后追踪下去你就会发现其实最后就是展示这个图片资源,因此我们可以确定这个图片资源应该就是转换后的图片,所以存储弱引用缓存应该是在转换图片后的操作。(这里我直接讲出存储所在的位置,如果想自己深究可以看看上篇文章图片加载流程中的“在主线程中显示图片”这个步骤的代码)最后我们会发现在EngineJob的notifyCallbacksOfResult方法中找到了弱引用缓存入口
EngineJob#notifyCallbacksOfResult
1 | void notifyCallbacksOfResult() { |
没错其入口就是我们在Glide图片加载流程提到过的回到主线程展示照片代码的前面,即回调了Engine的onEngineJobComplete来存储弱引用缓存
Engine#onEngineJobComplete
1 | public synchronized void onEngineJobComplete( |
ActiveResources#activate
1 | synchronized void activate(Key key, EngineResource<?> resource) { |
从这里也可以得到一个结论:正在使用的图片会存储到弱引用缓存中而不是LruCache缓存
获取LruCache缓存时存储
由于这个操作同时也涉及了LruCache的获取,故可以直接看下面对LruCache获取的解析
1.3 删除
弱引用缓存的删除其实体现在两处:
- JVM进行GC时
- 弱引用缓存对象引用计数为0时
JVM进行GC时
当JVM进行GC时,由于弱引用对象的特性,导致了弱引用缓存所关联的对象也会被回收,然后就会删除掉这个弱引用缓存对象,这部分我们在弱引用缓存获取的时候也分析过,这里不再进行解析(忘记的可以回头看看前面弱引用获取的分析)。
弱引用缓存对象引用计数为0时
细心的你不知有没有发现,其实在上面对缓存入口的分析时其实已经贴出了弱引用缓存删除的代码语句。不过为了方便阅读,在这里我还是再次直接贴出来。
EngineJob#notifyCallbacksOfResult
1 | //内部缓存存储的入口 |
EngineJob#decrementPendingCallbacks
1 | synchronized void decrementPendingCallbacks() { |
这里调用了EngineResource的release方法,让我们来看看
EngineResource#release
1 | void release() { |
在这里使用了著名的判断对象是否存活的算法-引用计数法,每次调用EngineResource对象的release方法,都会令该引用减一,当引用计数为0时,表示已经不再使用该对象,即图片不再使用时,就会回调Engine的onResourceReleased方法
Engine#onResourceReleased
1 | public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) { |
跟上面存储弱引用缓存时提到的发生GC的情况一样,最终会删除弱引用缓存,然后将该图片资源添加到LruCache缓存中。从这里也可以验证了我们上文提到的内存缓存的原理中的缓存实现:正在使用的图片使用弱引用机制进行缓存,不在使用中的图片使用LruCache来进行缓存。
2. LruCache缓存
2.1 获取
上文我们提到,获取内存缓存时,如果获取不到弱引用缓存时才会调用loadFromCache获取LruCache缓存。让我们看看Engine的loadFromCache方法
Engine#loadFromCache
1 | private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) { |
获取LruCache缓存跟弱引用缓存的获取操作很相似,首先调用了getEngineResourceFromCache来获取图片资源,然后将EngineResource的引用计数加1,并且还会将获取到的图片资源存储到弱引用缓存中。这里我们只分析getEngineResourceFromCache方法,因为调用ActiveResource的activate存储到弱引用缓存我们已经在上面弱引用缓存的存储中分析过了。
EngineResource#getEngineResourceFromCache
1 | /** |
在上面需注意的是该cache就是LruCache缓存的cache,另外你会发现获取图片缓存竟然不是调用cache的get方法,而是cache的remove方法,这就是Glide缓存策略的奇妙之处了。当获取到LruCache缓存的同时会删除掉该LruCache缓存,然后将该缓存存储到弱引用缓存中,这是为了保护这些图片不会被LruCache算法回收掉。
2.2 存储
当弱引用缓存删除时,会将缓存存储到LruCache缓存中。(分析可以看弱引用缓存删除操作)
2.3 删除
当获取LruCache缓存的同时对该LruCache缓存进行删除操作。(分析可以看LruCache缓存的获取操作)
2.5 小结
分析完内存缓存,你会发现弱引用缓存和LruCache缓存真的是环环相扣,密不可分,很多操作都是有关联性的。其流程图如下:
流程图的前提:开启内存缓存,关闭磁盘缓存
3. 磁盘缓存
3.1 作用
防止应用重复的从网络或从其它地方下载和读取数据
3.2 原理
使用Glide自定义的DiskLruCache算法
DiskLruCache算法是基于LruCache算法,该算法的应用场景是存储设备的缓存,即磁盘缓存。
3.3 配置
磁盘缓存也是默认开启的,默认情况下磁盘缓存的类型为DiskCacheStrategy.AUTOMATIC,当然可以通过代码关闭或者选择其它类型的磁盘缓存
1 | Glide.with(getContext()) |
diskCacheStrategyOf的相关参数说明如下:
参数 | 说明 |
---|---|
DiskCacheStrategy.AUTOMATIC |
这是默认的最优缓存策略: 本地:仅存储转换后的图片(RESOURCE) 网络:仅存储原始图片(DATA)。因为网络获取数据比解析磁盘上的数据要昂贵的多 |
DiskCacheStrategy.NONE | 不开启磁盘缓存 |
DiskCacheStrategy.RESOURCE | 缓存转换过后的图片 |
DiskCacheStrategy.DATA | 缓存原始图片,即原始输入流。它需要经过压缩转换,解析等操作才能最终展示出来 |
DiskCacheStrategy.ALL | 既缓存原始图片,又缓存转换后的图片 |
注:Glide加载图片默认是不会将一张原始图片展示出来的,而是将原始图片经过压缩转换,解析等操作。然后将转换后的图片展示出来。
3.4 源码解析
下列源码解析的前提:开启了磁盘缓存
虽然说上面的参数有五种,但其实我们只需要分析其中两种就能理解其它参数了。没错,接下来我们将分析RESOURCE类型和DATA类型。在分析前我们先回顾下Engine的load方法
Engine#load
1 | public synchronized <R> LoadStatus load( |
从上面可以发现如果获取不到内存缓存时,会开启线程来加载图片。从上篇文章Glide 4.9源码解析-图片加载流程我们可以知道,接下来会执行DecodeJob的run方法。
DecodeJob
1 | public void run() { |
可以发现在上述的方法中首先要找到对于场景的执行者然后执行任务。而执行者有三个,在上篇文章我们分析的是无缓存的情况,即网络获取数据的执行者。接下来我们就得分析获取转换后图片的执行者和获取原始突破的执行者。
1. RESOURCE缓存(转换图片)
1.1 获取
由于我们现在配置的缓存策略为RESOURCE,故对于执行者将是获取转换图片的执行者ResourceCacheGenerator,接下来会执行者会执行任务,让我们看看runGenerators方法
Engine#runGenerators
1 | private void runGenerators() { |
因为我们现在的执行者为ResourceCacheGenerator,所以会调用ResourceCacheGenerator的startNext来进行获取图片。
ResourceCacheGenerator#startNext
1 | public boolean startNext() { |
可以发现在ResourceCacheGenerator的startNext方法中,首先根据图片的参数,宽高等信息拿到缓存key,然后通过缓存key获取到磁盘上的文件,即磁盘缓存。最后通过加载器的loadData来处理获取到的磁盘缓存,由于磁盘缓存是File类型,根据Glide的注册表registry中可以找到该加载器其实是ByteBufferFileLoader(具体查找可看上篇博客的分析),故最后会调用ByteBufferFileLoader内部类ByteBufferFetcher的loadData方法来处理磁盘缓存。
ByteBufferFileLoader.ByteBufferFetcher#loadData
1 | public void loadData(@NonNull Priority priority, |
在loadData中会通过ByteBufferUtil工具类来获取对应磁盘文件的数据,然后通过回调ResourceCacheGenerator的onDataReady方法将数据回调出去。
ResourceCacheGenerator#onDataReady
1 | public void onDataReady(Object data) { |
进行回调DecodeJob的onDataFetcherReady
DecodeJob#onDataFetcherReady
1 | public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher, |
接下来就会对数据进行压缩转换等操作,然后进行展示(在上篇文章已经分析,这里不再进行后续的分析)。
1.2 存储
由于我们分析的是转换后的图片的存储,故其存储位置应该是在对原始图片压缩转换解析等一系列操作完后进行的,根据上一篇文章的分析,我们直接看DecodeJob的decodeFromRetrievedData方法
DecodeJob
1 | private void decodeFromRetrievedData() { |
我们在通知外界资源获取成功即notifyEncodeAndRelease方法中发现了RESOURCE类型的磁盘缓存的入口。
DecodeJob.DeferredEncodeManager#encode
1 | void encode(DiskCacheProvider diskCacheProvider, Options options) { |
在encode方法中通过DiskCacheProvider获取到DiskCache,然后调用put方法将图片资源缓存到磁盘上。
1.3 删除
由于缓存在了磁盘上,故删除不仅仅由代码控制。常见的删除方式如下:
- 用户主动删除手机上的对应文件
- 卸载软件
- 调用DiskCache.clear()
2. DATA缓存(原始图片)
2.1 获取
通过上面的分析我们知道原始图片对应的执行者为DataCacheGenerator,故还是会调用DataCacheGenerator的startNext方法来获取磁盘缓存
DataCacheGenerator#startNext
1 | public boolean startNext() { |
你会发现其实这个startNext方法与刚刚分析的ResourceCacheGenerator的startNext几乎是一样,不同的是缓存key的构建参数是不一样的,因为原始图片的缓存key是不需要图片的宽高,配置,变换等参数。然后接下来的分析与转换图片获取的分析是一致的,这里不再进行分析。
2.2 存储
我们知道原始图片说白了就是网络获取后得到的原始的输入流,通过上一篇加载流程的分析,我们知道获取到原始输入流是在HttpUrlFetcher的loadData方法中
HttpUrlFetcher#loadData
1 | public void loadData(@NonNull Priority priority, |
在获取到原始输入流后,会调用SourceGenerator的onDataReady将输入流回调出去
SourceGenerator#onDataReady
1 | public void onDataReady(Object data) { |
由于我们开启了磁盘缓存,故会将原始数据赋值给dataToCache,然后回调了DecodeJob的reschedule。
DecodeJob#reschedule
1 | public void reschedule() { |
这里继续回调了EngineJob的reschedule方法
EngineJob#reschedule
1 | public void reschedule(DecodeJob<?> job) { |
这里的job为DecodeJob,而getActiveSourceExecutor()会拿到线程池,所以reschedule方法其实会继续执行DecodeJob的run方法,然后拿到网络获取数据的执行者SourceGenerator,再次执行SourceGenerator的startNext方法(考虑到篇幅,不再贴其中的流程代码了,详细可以看上篇文章Glide 4.9源码解析-图片加载流程。
不过估计在这里可能有人会疑问,我们明明开启了磁盘缓存,为什么会获取到无缓存,网络获取数据的执行者呢?这是因为我们在存储原始图片的前提下,肯定是磁盘没有缓存,因此会从网络加载图片得到原始图片的输入流,然后回调,回调后当然还是拿到网络获取数据的执行者SourceGenerator。让我们再来看看这个startNext方法。
SourceGenerator#startNext
1 | public boolean startNext() { |
通过上面的分析我们知道此时的dataToCache是不为null的,而是原始图片,所以会调用cacheData方法将原始图片放入到磁盘缓存中。(如果你阅读了Glide图片加载流程的话,就会发现我们在图片加载流程的时候分析的其实是下面的代码,即没有开启缓存或获取不到磁盘缓存的情况)这里我们继续看cacheData方法
SourceGenerator#cacheData
1 | private void cacheData(Object dataToCache) { |
在cacheData方法中会通过DiskCache的put方法将缓存key,原始图片等存储到磁盘中。然后会构造DataCacheGenerator对象,这时候我们看回SourceGenerator的startNext方法,由于此时的sourceCacheGenerator已经是DataCacheGenerator对象了,所以会调用DataCacheGenerator的startNext方法来获取磁盘缓存中的原始图片。
2.3 删除
由于原始图片的缓存也属于磁盘缓存,故跟RESOURCE缓存一样删除不仅仅由代码控制,常见删除方式如下:
- 用户主动删除手机上的对应文件
- 卸载软件
- 调用DiskCache.clear()
3.5 小结
从上面的分析可以发现Glide中首先会读取转换后的图片的缓存,然后再读取原始图片的缓存。但是存储的时候恰恰相反,首先存储的是原始图片的缓存,再存储转换后的图片,不过获取和存储都受到Glide使用API的设置的影响。其流程图如下:
流程图的前提:关闭内存缓存或获取不到内存缓存,开启磁盘缓存
总结
通过对Glide缓存策略的分析,发现了Glide的缓存机制是多么的复杂,但又是多么的出色啊,所以用起来才会这么流畅和舒服。分析到这,Glide的缓存策略也就讲完了,而对Glide这个强大的图片开源库的源码分析也告一段落了,通过对Glide的图片加载流程和缓存策略的源码解析,让我更加佩服Glide这个强大的开源库。
参考文章: