本系列文章是在郭霖的文章基础上进行写作的
Glide缓存简介
Glide的缓存设计分为两个模块,一个是内存缓存,一个是硬盘缓存。
- 内存缓存作用是防止应用重复将图片数据读取到内存中
- 硬盘缓存的作用是防止应用重复从网络或其他地方重复下载和读取数据
缓存Key
既然是缓存功能,就必然会有用于进行缓存的Key。先来看一下Glide缓存Key的生成逻辑。
生成缓存Key的代码在Engine类的load()方法当中,这部分代码我们在上一篇文章当中已经分析过了,只不过当时忽略了缓存相关的内容,那么我们现在重新来看一下:
1 | public class Engine implements EngineJobListener, |
可以看到,这里在第11行调用了fetcher.getId()方法获得了一个id字符串,这个字符串也就是我们要加载的图片的唯一标识,比如说如果是一张网络上的图片的话,那么这个id就是这张图片的url地址。
接下来在第12行,将这个id连同着signature、width、height等等10个参数一起传入到EngineKeyFactory的buildKey()方法当中,从而构建出了一个EngineKey对象,这个EngineKey也就是Glide中的缓存Key了。
可见,决定缓存Key的条件非常多,即使你用override()方法改变了一下图片的width或者height,也会生成一个完全不同的缓存Key。EngineKey类的源码主要是重写了equals()和hashCode()方法。
内存缓存
默认情况下,Glide自动就是开启内存缓存的。也就是说,当我们使用Glide加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用Glide再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了,这样无疑就可以大幅度提升图片的加载效率。比方说你在一个RecyclerView当中反复上下滑动,RecyclerView中只要是Glide加载过的图片都可以直接从内存当中迅速读取并展示出来,从而大大提升了用户体验。
内存缓存功能是默认开始的,如果想关闭的话,Glide对此提供了接口:
1 | Glide.with(this) |
只需要调用skipMemoryCache()方法并传入true,就表示禁用掉Glide的内存缓存功能。
下面通过阅读源码来分析一下Glide的内存缓存功能是如何实现的。
其实说到内存缓存的实现,非常容易就让人想到LruCache算法(Least Recently Used),也叫近期最少使用算法。它的主要算法原理就是把最近使用的对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
那么不必多说,Glide内存缓存的实现自然也是使用的LruCache算法。不过除了LruCache算法之外,Glide还结合了一种弱引用的机制,共同完成了内存缓存功能。
首先回忆一下,在上一篇文章的第二步load()方法中,我们当时分析到了在loadGeneric()方法中会调用Glide.buildStreamModelLoader()方法来获取一个ModelLoader对象。当时没有再跟进到这个方法的里面再去分析,那么我们现在来看下它的源码:
1 | public class Glide { |
在第11行去构建ModelLoader对象的时候,先调用了一个Glide.get()方法,而这个方法就是关键。我们可以看到,get()方法中实现的是一个单例功能,而创建Glide对象则是在第24行调用GlideBuilder的createGlide()方法来创建的,那么我们跟到这个方法当中:
1 | public class GlideBuilder { |
这里也就是构建Glide对象的地方了。那么观察第22行,你会发现这里new出了一个LruResourceCache,并把它赋值到了memoryCache这个对象上面。你没有猜错,这个就是Glide实现内存缓存所使用的LruCache对象了。
现在创建好了LruResourceCache对象只能说是把准备工作做好了,接下来我们就一步步研究Glide中的内存缓存到底是如何实现的。
刚才在Engine的load()方法中我们已经看到了生成缓存Key的代码,而内存缓存的代码其实也是在这里实现的,那么我们重新来看一下Engine类load()方法的完整源码:
1 | public class Engine implements EngineJobListener, |
可以看到,这里在第17行调用了loadFromCache()方法来获取缓存图片,如果获取到就直接调用cb.onResourceReady()方法进行回调。如果没有获取到,则会在第26行调用loadFromActiveResources()方法来获取缓存图片,获取到的话也直接进行回调。只有在两个方法都没有获取到缓存的情况下,才会继续向下执行,从而开启线程来加载图片。
也就是说,Glide的图片加载过程中会调用两个方法来获取内存缓存,loadFromCache()和loadFromActiveResources()。这两个方法中一个使用的就是LruCache算法,另一个使用的就是弱引用。我们来看一下它们的源码:
1 | public class Engine implements EngineJobListener, |
在loadFromCache()方法的一开始,首先就判断了isMemoryCacheable是不是false,如果是false的话就直接返回null。这是什么意思呢?其实很简单,我们刚刚不是学了一个skipMemoryCache()方法吗?如果在这个方法中传入true,那么这里的isMemoryCacheable就会是false,表示内存缓存已被禁用。
接着调用了getEngineResourceFromCache()方法来获取缓存。在这个方法中,会使用缓存Key来从cache当中取值,而这里的cache对象就是在构建Glide对象时创建的LruResourceCache,那么说明这里其实使用的就是LruCache算法了。
但是,观察第22行,当我们从LruResourceCache中获取到缓存图片之后会将它从缓存中移除,然后在第16行将这个缓存图片存储到activeResources当中。activeResources就是一个弱引用的HashMap,用来缓存正在使用中的图片,我们可以看到,loadFromActiveResources()方法就是从activeResources这个HashMap当中取值的。使用activeResources来缓存正在使用中的图片,可以保护这些图片不会被LruCache算法回收掉。
好的,从内存缓存中读取数据的逻辑大概就是这些了。概括一下来说,就是如果能从内存缓存当中读取到要加载的图片,那么就直接进行回调,如果读取不到的话,才会开启线程执行后面的图片加载逻辑。
现在我们已经搞明白了内存缓存读取的原理,接下来的问题就是内存缓存是在哪里写入的呢?这里我们又要回顾一下上一篇文章中的内容了。还记不记得我们之前分析过,当图片加载完成之后,会在EngineJob当中通过Handler发送一条消息将执行逻辑切回到主线程当中,从而执行handleResultOnMainThread()方法。那么我们现在重新来看一下这个方法,代码如下所示:
1 | class EngineJob implements EngineRunnable.EngineRunnableManager { |
在第13行,这里通过EngineResourceFactory构建出了一个包含图片资源的EngineResource对象,然后会在第16行将这个对象回调到Engine的onEngineJobComplete()方法当中,如下所示:
1 | public class Engine implements EngineJobListener, |
可以看到,在第13行,回调过来的EngineResource被put到了activeResources当中,也就是在这里写入的缓存。
那么这只是弱引用缓存,还有另外一种LruCache缓存是在哪里写入的呢?这就要介绍一下EngineResource中的一个引用机制了。观察刚才的handleResultOnMainThread()方法,在第15行和第19行有调用EngineResource的acquire()方法,在第23行有调用它的release()方法。其实,EngineResource是用一个acquired变量用来记录图片被引用的次数,调用acquire()方法会让变量加1,调用release()方法会让变量减1,代码如下所示:
1 | class EngineResource<Z> implements Resource<Z> { |
也就是说,当acquired变量大于0的时候,说明图片正在使用中,也就应该放到activeResources弱引用缓存当中。而经过release()之后,如果acquired变量等于0了,说明图片已经不再被使用了,那么此时会在Z第24行调用listener的onResourceReleased()方法来释放资源,这个listener就是Engine对象,我们来看下它的onResourceReleased()方法:
1 | public class Engine implements EngineJobListener, |
可以看到,这里首先会将缓存图片从activeResources中移除,然后再将它put到LruResourceCache当中。这样也就实现了正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用LruCache来进行缓存的功能。
内存缓存的实现原理到此结束。
##硬盘缓存
不知道你还记不记得,在本系列的第一篇文章中我们就使用过硬盘缓存的功能了。当时为了禁止Glide对图片进行硬盘缓存而使用了如下代码:
1 | Glide.with(this) |
调用diskCacheStrategy()方法并传入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬盘缓存功能了。
这个diskCacheStrategy()方法基本上就是Glide硬盘缓存功能的一切,它可以接收四种参数:
- DiskCacheStrategy.NONE: 表示不缓存任何内容。
- DiskCacheStrategy.SOURCE: 表示只缓存原始图片。
- DiskCacheStrategy.RESULT:表示只缓存转换过后的图片(默认选项)。
- DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。
上面四种参数的解释本身并没有什么难理解的地方,但是有一个概念大家需要了解,就是当我们使用Glide去加载一张图片的时候,Glide默认并不会将原始图片展示出来,而是会对图片进行压缩和转换.总之就是经过种种一系列操作之后得到的图片,就叫转换过后的图片。而Glide默认情况下在硬盘缓存的就是转换过后的图片,我们通过调用diskCacheStrategy()方法则可以改变这一默认行为。
下面我们通过阅读源码来分析一下,Glide的硬盘缓存功能是如何实现的。
首先,和内存缓存类似,硬盘缓存的实现也是使用的LruCache算法,而且Google还提供了一个现成的工具类DiskLruCache。当然,Glide是使用的自己编写的DiskLruCache工具类,但是基本的实现原理都是差不多的。
接下来我们看一下Glide是在哪里读取硬盘缓存的。这里又需要回忆一下上篇文章中的内容了,Glide开启线程来加载图片后会执行EngineRunnable的run()方法,run()方法中又会调用一个decode()方法,那么我们重新再来看一下这个decode()方法的源码:
1 | private Resource<?> decode() throws Exception { |
可以看到,这里会分为两种情况,一种是调用decodeFromCache()方法从硬盘缓存当中读取图片,一种是调用decodeFromSource()来读取原始图片。默认情况下Glide会优先从缓存当中读取,只有缓存中不存在要读取的图片时,才会去读取原始图片。那么我们现在来看一下decodeFromCache()方法的源码,如下所示:
1 | private Resource > decodeFromCache() throws Exception { |
可以看到,这里会先去调用DecodeJob的decodeResultFromCache()方法来获取缓存,如果获取不到,会再调用decodeSourceFromCache()方法获取缓存,这两个方法的区别其实就是DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE这两个参数的区别,相信不需要我再做什么解释吧。
那么我们来看一下这两个方法的源码吧,如下所示:
1 | public Resource<Z> decodeResultFromCache() throws Exception { |
可以看到,它们都是调用了loadFromCache()方法从缓存当中读取数据,如果是decodeResultFromCache()方法就直接将数据解码并返回,如果是decodeSourceFromCache()方法,还要调用一下transformEncodeAndTranscode()方法先将数据转换一下再解码并返回.
然而我们注意到,这两个方法中在调用loadFromCache()方法时传入的参数却不一样,一个传入的是resultKey,另外一个却又调用了resultKey的getOriginalKey()方法。这个其实非常好理解,刚才我们已经解释过了,Glide的缓存Key是由10个参数共同组成的,包括图片的width、height等等。但如果我们是缓存的原始图片,其实并不需要这么多的参数,因为不用对图片做任何的变化。那么我们来看一下getOriginalKey()方法的源码:
1 | public Key getOriginalKey() { |
可以看到,这里其实就是忽略了绝大部分的参数,只使用了id和signature这两个参数来构成缓存Key。而signature参数绝大多数情况下都是用不到的,因此基本上可以说就是由id(也就是图片url)来决定的Original缓存Key。
搞明白了这两种缓存Key的区别,那么接下来我们看一下loadFromCache()方法的源码吧:
1 | private Resource<T> loadFromCache(Key key) throws IOException { |
这个方法的逻辑非常简单,调用getDiskCache()方法获取到的就是Glide自己编写的DiskLruCache工具类的实例,然后调用它的get()方法并把缓存Key传入,就能得到硬盘缓存的文件了。如果文件为空就返回null,如果文件不为空则将它解码成Resource对象后返回即可。
这样我们就将硬盘缓存读取的源码分析完了,那么硬盘缓存又是在哪里写入的呢?刚才已经分析过了,在没有缓存的情况下,会调用decodeFromSource()方法来读取原始图片。那么我们来看下这个方法:
1 | public Resource<Z> decodeFromSource() throws Exception { |
这个方法中只有两行代码,decodeSource()顾名思义是用来解析原图片的,而transformEncodeAndTranscode()则是用来对图片进行转换和转码的。我们先来看decodeSource()方法:
1 | private Resource<T> decodeSource() throws Exception { |
这里会在第5行先调用fetcher的loadData()方法读取图片数据,然后在第9行调用decodeFromSourceData()方法来对图片进行解码。接下来会在第18行先判断是否允许缓存原始图片,如果允许的话又会调用cacheAndDecodeSourceData()方法。而在这个方法中同样调用了getDiskCache()方法来获取DiskLruCache实例,接着调用它的put()方法就可以写入硬盘缓存了,注意原始图片的缓存Key是用的resultKey.getOriginalKey()。
好的,原始图片的缓存写入就是这么简单,接下来我们分析一下transformEncodeAndTranscode()方法的源码,来看看转换过后的图片缓存是怎么写入的。代码如下所示:
1 | private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) { |
这里的逻辑就更加简单明了了。先是在第3行调用transform()方法来对图片进行转换,然后在writeTransformedToCache()方法中将转换过后的图片写入到硬盘缓存中,调用的同样是DiskLruCache实例的put()方法,不过这里用的缓存Key是resultKey。
这样我们就将Glide硬盘缓存的实现原理也分析完了。虽然这些源码看上去如此的复杂,但是经过Glide出色的封装,使得我们只需要通过skipMemoryCache()和diskCacheStrategy()这两个方法就可以轻松自如地控制Glide的缓存功能了。
了解了Glide缓存的实现原理之后,接下来我们再来学习一些Glide缓存的高级技巧吧。
高级技巧
虽说Glide将缓存功能高度封装之后,使得用法变得非常简单,但同时也带来了一些问题。
如果在图片url地址的基础之上再加上一个token参数。也就是说,一张图片的url地址可能会是如下格式:
1 | http://url.com/image.jpg?token=d9caa6e02c990b0a |
而使用Glide加载这张图片的话,也就会使用这个url地址来组成缓存Key。但是接下来问题就来了,token作为一个验证身份的参数并不是一成不变的,很有可能时时刻刻都在变化。而如果token变了,那么图片的url也就跟着变了,图片url变了,缓存Key也就跟着变了。结果就造成了,明明是同一张图片,就因为token不断在改变,导致Glide的缓存功能完全失效了。
那么该如何解决这个问题呢?我们还是从源码的层面进行分析,首先再来看一下Glide生成缓存Key这部分的代码:
1 | public class Engine implements EngineJobListener, |
来看一下第11行,刚才已经说过了,这个id其实就是图片的url地址。那么,这里是通过调用fetcher.getId()方法来获取的图片url地址,而我们在上一篇文章中已经知道了,fetcher就是HttpUrlFetcher的实例,我们就来看一下它的getId()方法的源码吧,如下所示:
1 | public class HttpUrlFetcher implements DataFetcher<InputStream> { |
可以看到,getId()方法中又调用了GlideUrl的getCacheKey()方法。那么这个GlideUrl对象是从哪里来的呢?其实就是我们在load()方法中传入的图片url地址,然后Glide在内部把这个url地址包装成了一个GlideUrl对象。
很明显,接下来我们就要看一下GlideUrl的getCacheKey()方法的源码了,如下所示:
1 | public class GlideUrl { |
上面是代码简化后的,GlideUrl类的构造函数接收两种类型的参数,一种是url字符串,一种是URL对象。然后getCacheKey()方法中的判断逻辑非常简单,如果传入的是url字符串,那么就直接返回这个字符串本身,如果传入的是URL对象,那么就返回这个对象toString()后的结果。
因为getCacheKey()方法中的逻辑太直白了,直接就是将图片的url地址进行返回来作为缓存Key的。那么其实我们只需要重写这个getCacheKey()方法,加入一些自己的逻辑判断,就能轻松解决掉刚才的问题了。
创建一个MyGlideUrl继承自GlideUrl,代码如下所示:
1 | public class MyGlideUrl extends GlideUrl { |
可以看到,这里我们重写了getCacheKey()方法,在里面加入了一段逻辑用于将图片url地址中token参数的这一部分移除掉。这样getCacheKey()方法得到的就是一个没有token参数的url地址,从而不管token怎么变化,最终Glide的缓存Key都是固定不变的了。
当然,定义好了MyGlideUrl,我们还得使用它才行,将加载图片的代码改成如下方式即可:
1 | Glide.with(this) |
也就是说,我们需要在load()方法中传入这个自定义的MyGlideUrl对象,而不能再像之前那样直接传入url字符串了。不然的话Glide在内部还是会使用原始的GlideUrl类,而不是我们自定义的MyGlideUrl类。
#Glide的回调和监听
##回调的源码实现
我们都知道,使用Glide在界面上加载并展示一张图片只需要一行代码:
1 | Glide.with(this).load(url).into(imageView); |
其实在第二篇文章当中,我们已经分析了这一行代码背后的完整执行流程,但是这里我准备再带着大家单独回顾一下回调这部分的源码,这将有助于我们今天这篇文章的学习。
首先来看一下into()方法,这里我们将ImageView的实例传入到into()方法当中,Glide将图片加载完成之后,图片就能显示到ImageView上了。这是怎么实现的呢?我们来看一下into()方法的源码:
1 | public Target<TranscodeType> into(ImageView view) { |
可以看到,最后一行代码会调用glide.buildImageViewTarget()方法构建出一个Target对象,然后再把它传入到另一个接收Target参数的into()方法中。Target对象则是用来最终展示图片用的,如果我们跟进到glide.buildImageViewTarget()方法中,你会看到如下的源码:
1 | public class ImageViewTargetFactory { |
buildTarget()方法会根据传入的class参数来构建不同的Target对象,如果你在使用Glide加载图片的时候调用了asBitmap()方法,那么这里就会构建出BitmapImageViewTarget对象,否则的话构建的都是GlideDrawableImageViewTarget对象。至于上述代码中的DrawableImageViewTarget对象,这个通常都是用不到的,我们可以暂时不用管它。
之后就会把这里构建出来的Target对象传入到GenericRequest当中,而Glide在图片加载完成之后又会回调GenericRequest的onResourceReady()方法,我们来看一下这部分源码:
1 | public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback, |
这里在第14行调用了target.onResourceReady()方法,而刚才我们已经知道,这里的target就是GlideDrawableImageViewTarget对象,那么我们再来看一下它的源码:
1 | public class GlideDrawableImageViewTarget extends ImageViewTarget<GlideDrawable> { |
可以看到,这里在onResourceReady()方法中处理了图片展示,还有GIF播放的逻辑,那么一张图片也就显示出来了,这也就是Glide回调的基本实现原理。
接下来我们就来看一下在回调和监听方面还有哪些知识是可以扩展的。
into()方法
使用了这么久的Glide,我们都知道into()方法中是可以传入ImageView的。那么into()方法还可以传入别的参数吗?我可以让Glide加载出来的图片不显示到ImageView上吗?答案是肯定的,这就需要用到自定义Target功能。
其实通过上面的分析,我们已经知道了,into()方法还有一个接收Target参数的重载。即使我们传入的参数是ImageView,Glide也会在内部自动构建一个Target对象。而如果我们能够掌握自定义Target技术的话,就可以更加随心所欲地控制Glide的回调了。
我们先来看一下Glide中Target的继承结构图吧,如下所示:
可以看到,Target的继承结构还是相当复杂的,实现Target接口的子类非常多。如果我们要进行自定义的话,通常只需要在两种Target的基础上去自定义就可以了,一种是SimpleTarget,一种是ViewTarget。
接下来我就分别以这两种Target来举例,学习一下自定义Target的功能。
首先来看SimpleTarget,顾名思义,它是一种极为简单的Target,我们使用它可以将Glide加载出来的图片对象获取到,而不是像之前那样只能将图片在ImageView上显示出来。
那么下面我们来看一下SimpleTarget的用法示例:
1 | SimpleTarget<GlideDrawable> simpleTarget = new SimpleTarget<GlideDrawable>() { |
这里我们创建了一个SimpleTarget的实例,并且指定它的泛型是GlideDrawable,然后重写了onResourceReady()方法。在onResourceReady()方法中,我们就可以获取到Glide加载出来的图片对象了,也就是方法参数中传过来的GlideDrawable对象。有了这个对象之后你可以使用它进行任意的逻辑操作,这里我只是简单地把它显示到了ImageView上。
SimpleTarget的实现创建好了,那么只需要在加载图片的时候将它传入到into()方法中就可以了,现在运行一下程序,效果如下图所示。
虽然目前这个效果和直接在into()方法中传入ImageView并没有什么区别,但是我们已经拿到了图片对象的实例,然后就可以随意做更多的事情了。
当然,SimpleTarget中的泛型并不一定只能是GlideDrawable,如果你能确定你正在加载的是一张静态图而不是GIF图的话,我们还能直接拿到这张图的Bitmap对象,如下所示:
1 | SimpleTarget<Bitmap> simpleTarget = new SimpleTarget<Bitmap>() { |
可以看到,这里我们将SimpleTarget的泛型指定成Bitmap,然后在加载图片的时候调用了asBitmap()方法强制指定这是一张静态图,这样就能在onResourceReady()方法中获取到这张图的Bitmap对象了。
好了,SimpleTarget的用法就是这么简单,接下来我们学习一下ViewTarget的用法。
事实上,从刚才的继承结构图上就能看出,Glide在内部自动帮我们创建的GlideDrawableImageViewTarget就是ViewTarget的子类。只不过GlideDrawableImageViewTarget被限定只能作用在ImageView上,而ViewTarget的功能更加广泛,它可以作用在任意的View上。
这里我们还是通过一个例子来演示一下吧,比如我创建了一个自定义布局MyLayout,如下所示:
1 | public class MyLayout extends LinearLayout { |
在MyLayout的构造函数中,我们创建了一个ViewTarget的实例,并将Mylayout当前的实例this传了进去。ViewTarget中需要指定两个泛型,一个是View的类型,一个图片的类型(GlideDrawable或Bitmap)。然后在onResourceReady()方法中,我们就可以通过getView()方法获取到MyLayout的实例,并调用它的任意接口了。比如说这里我们调用了setImageAsBackground()方法来将加载出来的图片作为MyLayout布局的背景图。
接下来看一下怎么使用这个Target吧,由于MyLayout中已经提供了getTarget()接口,我们只需要在加载图片的地方这样写就可以了:
1 | public class MainActivity extends AppCompatActivity { |
就是这么简单,在into()方法中传入myLayout.getTarget()即可。现在重新运行一下程序,效果如下图所示。
好的,关于自定义Target的功能我们就介绍这么多,这些虽说都是自定义Target最基本的用法,但掌握了这些用法之后,你就能应对各种各样复杂的逻辑了。
proload()方法
Glide加载图片虽说非常智能,它会自动判断该图片是否已经有缓存了,如果有的话就直接从缓存中读取,没有的话再从网络去下载。但是如果我希望提前对图片进行一个预加载,等真正需要加载图片的时候就直接从缓存中读取,不想再等待慢长的网络加载时间了,这该怎么办呢?
对于很多Glide新手来说这确实是一个烦恼的问题,因为在没有学习本篇文章之前,into()方法中必须传入一个ImageView呀,而传了ImageView之后图片就显示出来了,这还怎么预加载呢?
不过在学习了本篇文章之后,相信你已经能够想到解决方案了。因为into()方法中除了传入ImageView之后还可以传入Target对象,如果我们在Target对象的onResourceReady()方法中做一个空实现,也就是不做任何逻辑处理,那么图片自然也就显示不出来了,而Glide的缓存机制却仍然还会正常工作,这样不就实现预加载功能了吗?
没错,上述的做法完全可以实现预加载功能,不过有没有感觉这种实现方式有点笨笨的。事实上,Glide专门给我们提供了预加载的接口,也就是preload()方法,我们只需要直接使用就可以了。
preload()方法有两个方法重载,一个不带参数,表示将会加载图片的原始尺寸,另一个可以通过参数指定加载图片的宽和高。
preload()方法的用法也非常简单,直接使用它来替换into()方法即可,如下所示:
1 | Glide.with(this) |
需要注意的是,我们如果使用了preload()方法,最好要将diskCacheStrategy的缓存策略指定成DiskCacheStrategy.SOURCE。因为preload()方法默认是预加载的原始图片大小,而into()方法则默认会根据ImageView控件的大小来动态决定加载图片的大小。因此,如果不将diskCacheStrategy的缓存策略指定成DiskCacheStrategy.SOURCE的话,很容易会造成我们在预加载完成之后再使用into()方法加载图片,却仍然还是要从网络上去请求图片这种现象。
调用了预加载之后,我们以后想再去加载这张图片就会非常快了,因为Glide会直接从缓存当中去读取图片并显示出来,代码如下所示:
1 | Glide.with(this) |
注意,这里我们仍然需要使用diskCacheStrategy()方法将硬盘缓存策略指定成DiskCacheStrategy.SOURCE,以保证Glide一定会去读取刚才预加载的图片缓存。
preload()方法的用法大概就是这么简单,但是仅仅会使用显然层次有些太低了,下面我们就满足一下好奇心,看看它的源码是如何实现的。
和into()方法一样,preload()方法也是在GenericRequestBuilder类当中的,代码如下所示:
1 | public class GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> implements Cloneable { |
正如刚才所说,preload()方法有两个方法重载,你可以调用带参数的preload()方法来明确指定图片的宽和高,也可以调用不带参数的preload()方法,它会在内部自动将图片的宽和高都指定成Target.SIZE_ORIGINAL,也就是图片的原始尺寸。
然后我们可以看到,这里在第5行调用了PreloadTarget.obtain()方法获取一个PreloadTarget的实例,并把它传入到了into()方法当中。从刚才的继承结构图中可以看出,PreloadTarget是SimpleTarget的子类,因此它是可以直接传入到into()方法中的。
那么现在的问题就是,PreloadTarget具体的实现到底是什么样子的了,我们看一下它的源码,如下所示
1 | public final class PreloadTarget<Z> extends SimpleTarget<Z> { |
PreloadTarget的源码非常简单,obtain()方法中就是new了一个PreloadTarget的实例而已,而onResourceReady()方法中也没做什么事情,只是调用了Glide.clear()方法。
这里的Glide.clear()并不是清空缓存的意思,而是表示加载已完成,释放资源的意思,因此不用在这里产生疑惑。
downloadOnly()方法
一直以来,我们使用Glide都是为了将图片显示到界面上。虽然我们知道Glide会在图片的加载过程中对图片进行缓存,但是缓存文件到底是存在哪里的,以及如何去直接访问这些缓存文件?我们都还不知道。
其实Glide将图片加载接口设计成这样也是希望我们使用起来更加的方便,不用过多去考虑底层的实现细节。但如果我现在就是想要去访问图片的缓存文件该怎么办呢?这就需要用到downloadOnly()方法了。
和preload()方法类似,downloadOnly()方法也是可以替换into()方法的,不过downloadOnly()方法的用法明显要比preload()方法复杂不少。顾名思义,downloadOnly()方法表示只会下载图片,而不会对图片进行加载。当图片下载完成之后,我们可以得到图片的存储路径,以便后续进行操作。
那么首先我们还是先来看下基本用法。downloadOnly()方法是定义在DrawableTypeRequest类当中的,它有两个方法重载,一个接收图片的宽度和高度,另一个接收一个泛型对象,如下所示:
- downloadOnly(int width, int height)
- downloadOnly(Y target)
这两个方法各自有各自的应用场景,其中downloadOnly(int width, int height)是用于在子线程中下载图片的,而downloadOnly(Y target)是用于在主线程中下载图片的。
那么我们先来看downloadOnly(int width, int height)的用法。当调用了downloadOnly(int width, int height)方法后会立即返回一个FutureTarget对象,然后Glide会在后台开始下载图片文件。接下来我们调用FutureTarget的get()方法就可以去获取下载好的图片文件了,如果此时图片还没有下载完,那么get()方法就会阻塞住,一直等到图片下载完成才会有值返回。
下面我们通过一个例子来演示一下吧,代码如下所示:
1 | public void downloadImage(View view) { |
首先刚才说了,downloadOnly(int width, int height)方法必须要用在子线程当中,因此这里的第一步就是new了一个Thread。在子线程当中,我们先获取了一个Application Context,这个时候不能再用Activity作为Context了,因为会有Activity销毁了但子线程还没执行完这种可能出现。
接下来就是Glide的基本用法,只不过将into()方法替换成了downloadOnly()方法。downloadOnly()方法会返回一个FutureTarget对象,这个时候其实Glide已经开始在后台下载图片了,我们随时都可以调用FutureTarget的get()方法来获取下载的图片文件,只不过如果图片还没下载好线程会暂时阻塞住,等下载完成了才会把图片的File对象返回。
最后,我们使用runOnUiThread()切回到主线程,然后使用Toast将下载好的图片文件路径显示出来。
现在重新运行一下代码,效果如下图所示。
这样我们就能清晰地看出来图片完整的缓存路径是什么了。
之后我们可以使用如下代码去加载这张图片,图片就会立即显示出来,而不用再去网络上请求了:
1 | public void loadImage(View view) { |
需要注意的是,这里必须将硬盘缓存策略指定成DiskCacheStrategy.SOURCE或者DiskCacheStrategy.ALL,否则Glide将无法使用我们刚才下载好的图片缓存文件。
那么这个downloadOnly(int width, int height)方法的工作原理到底是什么样的呢?我们来简单快速地看一下它的源码吧。
首先在DrawableTypeRequest类当中可以找到定义这个方法的地方,如下所示:
1 | public class DrawableTypeRequest<ModelType> extends DrawableRequestBuilder<ModelType> |
这里会先调用getDownloadOnlyRequest()方法得到一个GenericTranscodeRequest对象,然后再调用它的downloadOnly()方法,代码如下所示:
1 | public class GenericTranscodeRequest<ModelType, DataType, ResourceType> |
这里又是调用了一个getDownloadOnlyRequest()方法来构建了一个图片下载的请求,getDownloadOnlyRequest()方法会返回一个GenericRequestBuilder对象,接着调用它的into(width, height)方法,我们继续跟进去瞧一瞧:
1 | public FutureTarget<TranscodeType> into(int width, int height) { |
可以看到,这里首先是new出了一个RequestFutureTarget对象,RequestFutureTarget也是Target的子类之一。然后通过Handler将线程切回到主线程当中,再将这个RequestFutureTarget传入到into()方法当中。
那么也就是说,其实这里就是调用了接收Target参数的into()方法,然后Glide就开始执行正常的图片加载逻辑了。那么现在剩下的问题就是,这个RequestFutureTarget中到底处理了些什么逻辑?我们打开它的源码看一看:
1 | public class RequestFutureTarget<T, R> implements FutureTarget<R>, Runnable { |
刚才我们已经学习过了downloadOnly()方法的基本用法,在调用了downloadOnly()方法之后,再调用FutureTarget的get()方法,就能获取到下载的图片文件了。而downloadOnly()方法返回的FutureTarget对象其实就是这个RequestFutureTarget,因此我们直接来看它的get()方法就行了。
RequestFutureTarget的get()方法中又调用了一个doGet()方法,而doGet()方法才是真正处理具体逻辑的地方。首先在doGet()方法中会判断当前是否是在子线程当中,如果不是的话会直接抛出一个异常。然后下面会判断下载是否已取消、或者已失败,如果是已取消或者已失败的话都会直接抛出一个异常。接下来会根据resultReceived这个变量来判断下载是否已完成,如果这个变量为true的话,就直接把结果进行返回。
那么如果下载还没有完成呢?我们继续往下看,接下来就进入到一个wait()当中,把当前线程给阻塞住,从而阻止代码继续往下执行。这也是为什么downloadOnly(int width, int height)方法要求必须在子线程当中使用,因为它会对当前线程进行阻塞,如果在主线程当中使用的话,那么就会让主线程卡死,从而用户无法进行任何其他操作。
那么现在线程被阻塞住了,什么时候才能恢复呢?答案在onResourceReady()方法中。可以看到,onResourceReady()方法中只有三行代码,第一行把resultReceived赋值成true,说明图片文件已经下载好了,这样下次再调用get()方法时就不会再阻塞线程,而是可以直接将结果返回。第二行把下载好的图片文件赋值到一个全局的resource变量上面,这样doGet()方法就也可以访问到它。第三行notifyAll一下,通知所有wait的线程取消阻塞,这个时候图片文件已经下载好了,因此doGet()方法也就可以返回结果了。
好的,这就是downloadOnly(int width, int height)方法的基本用法和实现原理,那么下面我们来看一下downloadOnly(Y target)方法。
回想一下,其实downloadOnly(int width, int height)方法必须使用在子线程当中,最主要还是因为它在内部帮我们自动创建了一个RequestFutureTarget,是这个RequestFutureTarget要求必须在子线程当中执行。而downloadOnly(Y target)方法则要求我们传入一个自己创建的Target,因此就不受RequestFutureTarget的限制了。
但是downloadOnly(Y target)方法的用法也会相对更复杂一些,因为我们又要自己创建一个Target了,而且这次必须直接去实现最顶层的Target接口,比之前的SimpleTarget和ViewTarget都要复杂不少。
那么下面我们就来实现一个最简单的DownloadImageTarget吧,注意Target接口的泛型必须指定成File对象,这是downloadOnly(Y target)方法要求的,代码如下所示:
1 | public class DownloadImageTarget implements Target<File> { |
由于是要直接实现Target接口,因此需要重写的方法非常多。这些方法大多是数Glide加载图片生命周期的一些回调,我们可以不用管它们,其中只有两个方法是必须实现的,一个是getSize()方法,一个是onResourceReady()方法。
在第二篇Glide源码解析的时候,我带着大家一起分析过,Glide在开始加载图片之前会先计算图片的大小,然后回调到onSizeReady()方法当中,之后才会开始执行图片加载。而这里,计算图片大小的任务就交给我们了。只不过这是一个最简单的Target实现,我在getSize()方法中就直接回调了Target.SIZE_ORIGINAL,表示图片的原始尺寸。
然后onResourceReady()方法我们就非常熟悉了,图片下载完成之后就会回调到这里,我在这个方法中只是打印了一下下载的图片文件的路径。
这样一个最简单的DownloadImageTarget就定义好了,使用它也非常的简单,我们不用再考虑什么线程的问题了,而是直接把它的实例传入downloadOnly(Y target)方法中即可,如下所示:
1 | public void downloadImage(View view) { |
现在重新运行一下代码并点击Download Image按钮,然后观察控制台日志的输出,结果如下图所示。
这样我们就使用了downloadOnly(Y target)方法同样获取到下载的图片文件的缓存路径了。
listener()方法
其实listener()方法的作用非常普遍,它可以用来监听Glide加载图片的状态。举个例子,比如说我们刚才使用了preload()方法来对图片进行预加载,但是我怎样确定预加载有没有完成呢?还有如果Glide加载图片失败了,我该怎样调试错误的原因呢?答案都在listener()方法当中。
首先来看下listener()方法的基本用法吧,不同于刚才几个方法都是要替换into()方法的,listener()是结合into()方法一起使用的,当然也可以结合preload()方法一起使用。最基本的用法如下所示:
1 | public void loadImage(View view) { |
这里我们在into()方法之前串接了一个listener()方法,然后实现了一个RequestListener的实例。其中RequestListener需要实现两个方法,一个onResourceReady()方法,一个onException()方法。从方法名上就可以看出来了,当图片加载完成的时候就会回调onResourceReady()方法,而当图片加载失败的时候就会回调onException()方法,onException()方法中会将失败的Exception参数传进来,这样我们就可以定位具体失败的原因了。
没错,listener()方法就是这么简单。不过还有一点需要处理,onResourceReady()方法和onException()方法都有一个布尔值的返回值,返回false就表示这个事件没有被处理,还会继续向下传递,返回true就表示这个事件已经被处理掉了,从而不会再继续向下传递。举个简单点的例子,如果我们在RequestListener的onResourceReady()方法中返回了true,那么就不会再回调Target的onResourceReady()方法了。
关于listener()方法的用法就讲这么多,不过还是老规矩,我们再来看一下它的源码是怎么实现的吧。
首先,listener()方法是定义在GenericRequestBuilder类当中的,而我们传入到listener()方法中的实例则会赋值到一个requestListener变量当中,如下所示:
1 | public class GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> implements Cloneable { |
接下来在构建GenericRequest的时候这个变量也会被一起传进去,最后在图片加载完成的时候,我们会看到如下逻辑:
1 | public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback, |
可以看到,这里在第11行会先回调requestListener的onResourceReady()方法,只有当这个onResourceReady()方法返回false的时候,才会继续调用Target的onResourceReady()方法,这也就是listener()方法的实现原理。
另外一个onException()方法的实现机制也是一模一样的,代码同样是在GenericRequest类中,如下所示:
1 | public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback, |
可以看到,这里会在第9行回调requestListener的onException()方法,只有在onException()方法返回false的情况下才会继续调用setErrorPlaceholder()方法。也就是说,如果我们在onException()方法中返回了true,那么Glide请求中使用error(int resourceId)方法设置的异常占位图就失效了。
这样我们也就将listener()方法的全部实现原理都分析完了。