Glide-平滑加载的图片框架 [源码]
本文按照 Glide
常用的如下用法来分析源码:
1 | Glide.with(context).load("url").thumbnail().into(imageView); |
根据上面的代码,将内容分为以下几个章节:
Glide
- 强大的顶层管理类Glide.with()
- 创建及缓存RequestManager
.load(url)
- 创建RequestBuilder
.thumbnail()
- 配置RequestBuilder
.into(target)
- 发起Request
- 开始加载资源
- 缓存和数据源
- 数据转换
- 总结
强大的 Glide
Glide
是一个单例对象,在这个框架中功能相对复杂,每个功能都由对应的类实现,但是他们都被 Glide
类统一管理,Glide
的构造方法很长,主要是初始化一些内部持有的管理类,类比来看的 Glide
就类似高级管理阶层,下属还有很多次级管理层。
简单看一下重要的成员变量:
1 | Glide.java |
Glide.with()
Glide.with()
方法将会返一个RequestManager
对象。
在讨论 Glide.with()
之前,先来关注一下两个比较重要的点:
- 借助
Fragment
同步与宿主Activity
的生命周期及内存管理。 RequestManager
的创建和缓存
借助 Fragment
当加载图片时,我们需要时刻观察宿主 Activity
的状态:
- 结合生命周期,对后台线程做一些耗时操作,比如网络请求、IO 流、图片的编解码等,在适当的时机进行暂停、重启或销毁操作;
- 关注内存变化,当回调
onTrimMemory()
等内存回收方法时,框架内部能够感知到;
需要做到这些,就需要在 Activity
的各种方法中调用框架对应的方法,但是这样造成很强的耦合,使用起来会变的很繁琐,不过 Glide
使用了一种比较巧妙的办法那就是在对应的 Activity
中添加一个空的 Fragment
,这个 Fragment
不会绘制任何 UI
,但是由于 Fragment
和 Activity
的特殊关系,当这个 Fragment
被添加到 Activity
中时,他的生命周期就与 Activity
的生命周期同步了,内部只需要关注 Fragment
的生命周期即可,而对外是完全封闭的。
在 Glide
内部提供了 RequestManagerFragment
和 SupportRequestManagerFragment
对 v4.Fragment
做了兼容,他们主要关注的是如下方法:
1 | // 生命周期方法 |
RequestManager 的缓存
在 Fragment
内部持有一个 RequestManager
,当 Fragment
的生命周期+内存回收方法被回调时,RequestManager
的对应方法会被调用,从而实现对请求和内存的管理。
我们希望在每个 Activity
中仅有一个 RequestManager
来统一处理,而不是每次都创建一个新的,因此我们要对 RequestManager
做一个缓存,而这个缓存也是借助 Fragment
来实现的,每次向 Activity
中添加 Fragment
时,总会使用唯一的 tag
,这样需要使用 RequestManager
时,会先去拿对应 tag
的 Fragment
,拿到了说明已经创建过,取出 RequestManager
直接使用,否则创建新的 Fragment
(持有 RequestManager
)用指定的 tag
添加到 Activity
中,同时拿到 RequestManager
。
因为 v4.Fragment
的问题,所有的方法都会有一个 support
的对应方法,以下仅保留关键代码:
1 | // RequestManagerRetriever.java |
所有图片的加载总是以 Glide.with()
方法开始,当调用 Glide.with()
方法时,会借助单例 RequestManagerRetriever
来获取到一个 RequestManager
,当然这里不仅仅是单纯的获取、还包括缓存及同步生命周期等,这个在上面已经说过了。
1 | public static RequestManager with(Context context) { |
RequestManagerRetriever
是一个单例,他对 RequestManagerFragment
及其内部的 RequestManager
进行管理,Retriever
是猎犬、寻回的意思,RequestManagerRetriever
也就是对 RequestManager
进行寻回,找到已经在使用的 RequestManager
,可以说很形象啦。当需要获取 RequestManager
时,借助这条 猎犬 即可,他会给你找回合适的 RequestManager
。
RequestManager
RequestManager
主要用来管理请求的发送和停止,
内部有一个 ConnectivityMonitor
它使用 BroadcastReceiver
实现,用于检测网络变化。
1 | ConnectivityMonitor connectivityMonitor = factory.build(context, |
使用 RequestTracker
来真正的管理请求队列
1 | private final Set<Request> requests = Collections.newSetFromMap(new WeakHashMap<Request, Boolean>()); |
.load(Type param)
load(Type param)
将会返回一个DrawableTypeRequest
对象,他并不是一个真正的Request
,更像一个RequestBuilder
,其实他真的是GenericRequestBuilder
的子类,是建造者模式的体现,用来进行其他参数的配置。
利用 Glide.with()
此时我们已经获取到了合适的 RequestManager
对象,可以使用 .load()
方法选择加载文件、Uri
、网络路径等:
1 | RequestManager.java |
可以看到每种数据类型会对应不同的 load(Type param)
方法,同时又会对应一个 fromType()
方法来构造 DrawableTypeRequest
对象,而他们最终都会使用 loadGeneric()
方法来创建一个 DrawableTypeRequest
1 | RequestManager.java |
ModelLoader
这里可以看到有 ModelLoader
,他负责完成数据转换的功能,例如想要加载网络图片,会提供一个 url
那么就会有一个 ModelLoader
完成 String-> InputStream
的转化,通过网络请求拿到 InputStream
。
这里会有更丰富的内容,统一在数据转换一节中讨论,这里需要知道的是他作为 DrawableTypeRequest
的成员,在将来会承担数据转换的工作。
DataLoadProvider
在上面我们创建了一个 DrawableTypeRequest
,看一下构造方法
1 | DrawableTypeRequest(Class<ModelType> modelClass, ModelLoader<ModelType, InputStream> streamModelLoader, |
发现他需要一个 DataLoadProvider
,他也是起到了一个数据转换的作用,不过它是提供编解码器来将 InputStream
等格式转化成 Bitmap
及 GifDrawable
等格式。
这里会有更丰富的内容,统一在数据转换一节中讨论,这里需要知道的是他作为 DrawableTypeRequest
的成员,在将来会承担数据转换的工作。
.thumbnail()
.thumbnail()
等配置项都是GenericRequestBuilder
的成员方法,返回值是GenericRequestBuilder
,达到链式调用的目的。
这里只是用 thumbnial
来代指一系列的配置请求的方法,实际上配置项要更多,大致列举如下:
1 | // 控制图片尺寸 |
以上的一系列方法都是一些配置项,调用之后作为参数存储起来,请求的时候使用
1 | private Key signature = EmptySignature.obtain(); // 对应 signature() |
.into
.into()
和.preload()
方法会返回Target
对象。
调用 into()
方法后,意味着构造已经完成,将会创建请求开始加载图片,本次图片加载的流程也进入了下一个阶段–请求阶段。
当最后调用 into()
方法或者 preload()
方法时,将会从这个 target
中取出之前在发送的请求,然后清除掉,并构建新的 Request
并发起请求。
1 | public <Y extends Target<TranscodeType>> Y into(Y target) { |
使用所有配置的参数构建 Request
,主要还是有缩略图的情况要特殊处理一下,因为缩略图其实和原图是两个独立的 Request
,两个请求同时创建出来,同时缩略图请求优先级高于原图请求,因此要特殊处理。
1 | private Request buildRequestRecursive(Target<TranscodeType> target, ThumbnailRequestCoordinator parentCoordinator) { |
开始加载资源
其实这里的请求是一个更广义上的概念,虽然我们大多情况下会加载一个网络图片,但是这里的请求意思是对资源的请求,这个资源可能是一个文件、一张网络图片或者是一个 Uri
,不要先入为主的觉得是一个网络请求。
到目前为止,经历了一系列的配置,请求被构建出来了,接下来是加载请求阶段,在这之前可以对之前的一些关键点做一下总结:
- 所有请求都会通过
RequestManager
来管理,他监控宿主的生命周期,并调整请求的开始和结束,这一切都是通过它内部RequestTracker
来完成的,作为RequestManager
的一个成员,他负责了请求队列的管理(发起和暂停等),是真正意义上的请求管理类。 - 使用配置的参数构建出来的对象是
GenericRequest
,他是一个真正的请求,上面我们看到调用了requestTracker.runRequest()
方法,里面调用了request.begin()
方法,请求从这里开始。
在发起请求时会调用 onSizeReady
方法,在方法内部,从 DataLoadProivder
中取得 ModelLoader
进而获取到 DataFetcher
他是获取数据的工具类,然后还有 ResourceTranscoder
用来转码操作。
1 | GenericRequest.java |
缓存和数据源
最关键的地方是使用 engine.load()
触发加载,这个 Engine
是 Glide
的成员,之前被传递到 Request
中的,简单看一下 load()
方法中的伪代码,表面看起来分成了 3 步:
- loadFromCache()
- loadFromActiveResources()
- EngineJob.run()
前面两步是从内存缓存中获取数据,EngineJob
内部则是从文件缓存和数据源中获取,因为涉及耗时操作,这部分会在线程池中执行。
1 | Engine.java |
当使用 engine.load()
时,就会开始获取资源,资源的获取会按照 内存缓存 -> 文件缓存 -> 原始数据
这样的顺序去查找, Glide
采用还是常用的内存、文件两级缓存的方式,针对资源返回的位置,可以分为如下几个来源:
LruCache<Key,Resource>
- 基于 Lru 算法的内存缓存,在超出指定内存时,会进行清理Map<Key,WeakRef<Resource>>
- 虚引用做的内存缓存,系统内存不足时回收资源DiskLruCache
- 基于 Lru 算法做的文件缓存Source
- 数据源,可能是文件,可能是网络图,他是最原始的目标文件
其中内存缓存和文件缓存对应的数据结构如下:
1 | Engine.java |
内存缓存
内存缓存方案的选择,考虑如下几个方面:
LruCache
的优点是可以有效的将内存控制在一个范围内,并优先保留最后使用的图片,缺点是当内存到达界限时就会开始回收,长列表时有可能会将正在显示的图片回收掉。WeakRef
的优点是在发生gc时,会回收只有弱引用的资源,并且通过cleanReferenceQueue
我们可以观察到被回收的动作。- 当产生大量加载事件时,会不停的开辟和回收资源占用的内存空间,造成显著的内存抖动。
更优化的内存缓存方案应该满足 3 个条件:
- 保证正在使用的图片不会被回收,如果图片不被使用了要及时缓存到别的缓存中
- 对没有使用的资源有一个上限管控,既能保证缓存的优势,又不会对内存空间造成压力
- 减少内存空间的开辟操作,能够尽量复用已经开辟好的内存空间
Glide
选择的缓存方案是将内存缓存分成两级,同时用一个 BitmapPool
来保留已经开辟的内存空间:
- activeResouces
Map<Key, WeakRef<Resource>>
用来存储正在使用的资源 - lruCache
LruCache<Key, Resource>
用来存储没有引用计数的资源 - BitmapPool
Lru<Bitmap>
每次创建Bitmap
都会优先从BitmapPool
中查找可以复用的内存空间
资源的存储:当从文件缓存或 Source
中获取到目标资源时,存储到 activeResouces
中,因为该资源处于被获取使用的状态,当然除了 activeResouces
的 WeakRef
它还被展示它的 View
持有,具有一个强引用,这保证了即使发生 gc
也只会回收掉 activeResouces
中那些不被 View
引用的资源。
资源的获取:先从 lruCache
中获取,拿到资源后从 lruCache
删除,加入 activeResouces
中,保证该资源不会因为达到 lruCache
的内存上限而被回收。
资源的回收:当资源被触发回收时,计算引用数,没有引用就从 activeResouces
移除,存储到 lruCache
中,方便下次可以从内存中读取到。
如何判断资源没有引用了?
启动一个后台线程不停的遍历 ReferenceQueue
一旦发现被系统回收的资源,立刻重新构建(引用数变为0)并且通知资源被 release 了,加入到 LRU 缓存中
文件缓存
文件缓存选择的是 DiskLruCache
,是一个基于 Lru
算法的文件缓存方案,在 Android 上有比较广泛的应用,原理这里不展开说了。
文件缓存的查找在 EngineRunnable
的 run()
方法中:
1 | EngineRunnable.java |
从文件缓存读取和从数据源读取都在 run()
方法中实现,这里加载的策略取决于一个 Stage
的枚举类型,他有两个值:
- CACHE:表示从文件缓存中读取资源
- SOURCE:表示从数据源中读取资源,数据源是指网络资源、原始文件等
初始值永远是 Stage.CACHE
意味着总是尝试先从文件缓存中读取,注意这个枚举决定了是从 缓存文件 加载还是从 数据源 加载,不要和下面的弄混了。
当从文件缓存中读取时,又需要关注一个 DiskCacheStrategy
的枚举,在构建 Glide
的请求时我们可能会配置这个加载策略,它由两个内部字段构成
- cacheSource:意为缓存和加载原始缓存数据
- cacheResult:意为缓存和加载转换后的缓存数据,这个转换包括根据宽高压缩、transform 转换等
对于文件缓存加载来说,如果不 强制指定 总是优先 cacheResult
如果获取不到再 cacheSource
从原始缓存数据中加载并作转换操作,注意这个枚举决定了是加载 原始文件 还是加载 转换后 的文件。下面分为 result
和 source
两个流程比较一下差别:
- result:
Key
->Disk 获取
->cacheDecoder
解码 ->transcode()
转码 -> 返回数据 - source:
Key.getOriginKey()
->Disk
获取 ->cacheDecoder
解码 -> 使用Key
写入文件缓存 ->transcode()
转码 -> 返回数据
对比上面的过程,主要取决于 Key
, 这个 Key
是使用 id/signature/width/height
等组成的联合键,而 Key.getOriginKey()
则只比对 id/signature
从而达到缓存原始数据 (Source
)和 结果数据 (Result
)
数据源
当使用 State.CACHE
策略从文件缓存中加载失败时,将会转换成 Stage.SOURCE
,并重新运行当前 EngineRunnable
,从而开始从数据源加载数据
1 | private void onLoadFailed(Exception e) { |
数据源数据的加载由 DataFetcher
来完成,DataFetcher
是在 Glide
初始化时注册的 ModelLoaderFactory
生成的,这个在后面会再细说。
借助 fetcher.loadData()
可以获取到数据源的原始数据,此时根据 DiskCacheStrategy
的不同,也分为两个不同的缓存渠道:
- result:
Key
->SourceDecoder
解码原始数据 ->transform()
转换 -> 使用Key
写入文件缓存 ->transcode()
转码 - source:
Key.getOriginalKey()
->SourceEncoder
编码写入Disk
->Disk
获取 ->cacheDecoder
解码 ->transform()
转换 -> 使用Key
写入文件缓存 ->transcode()
转码
关于缓存的总结
上面涉及了一些编解码、转换转码等操作,我们不需要格外关注,只需要知道他是对数据的一种变换,我们在理解整个流程时,不需要太关注那些编解码、写文件等操作,只要能够理清整体缓存流程,理解其中的设计原理。
为了更好的理解,下面对一些步骤进行简单的解释:
Key
是由id/signature/width/height
组成的联合键,作为缓存文件的键值,他是非常精确的,而Key.getOriginKey()
只关心id/signature
,是更通用的键值。fetcher
类似数据加载器,提前配置好的,由GenericLoaderFactory
管理,用来加载数据,可以简单理解为通过网络Url
转换成InputStream
流的过程SourceDecoder 解码原始数据
是说将fetcher
拿回来的数据进行解码,可以简单理解为将InputStream
转换为Bitmap
的过程SourceEncoder 编码写入 Disk
将fetcher
返回的数据进行编码操作,输出到OutputStream
,可以简单理解为网络请求返回的InputStream
转换成outputStream
的过程。cacheDecoder
解码,cacheEncoder
是之前就配置好的一个转码器,由DataLoadProvider
提供,主要负责将文件转换成目标数据,可以简单理解为将文件转为Bitmap
类似的操作transcode()
转码,这是一个转码操作,也是之前就配置好的,由DataLoadProviderRegistry
管理,可以简单理解为将Bitmap
转换为GlideDrawable
这种数据的过程transform() 转换
在构建Glide
请求时配置的,是自定义,比如转换成圆角、圆形图之类的操作
针对 内存 -> 文件 -> 数据源 的整个流程,整理一个流程图,展示一下整个数据加载的过程。
数据转换
上面页陆陆续续提到了一些编解码前面提到了几种进行数据转换的类,这里汇总整理介绍,更直观的归纳一下这几种数据转化的作用和使用的时机。
ModelLoader
负责将String
、Uri
、int
、URL
等等类型转化成InputStream
和ParcelFileDescriptor
,是整个转化流程的第一步,他的原始数据类型都是我们调用load()
方法里面支持的数据类型。DataLoadProvider
负责编解码的转换,它提供Encoder
和Decoder
,可以将InputStream
和ParcelFileDescriptor
转换成Bitmap
、GifDrawable
等,很明显,是接着上一步进行的一个编解码操作,他的原始数据类型,就是ModelLoader
的目标数据类型。ResourceTranscoder
负责结果数据类型的转换,可以将Bitmap
和GifDrawable
转换成GlideDrawable
等,原始数据类型就是上一步的目标数据类型。
ModelLoader
ModelLoader
用来针对某种数据源和返回数据创建一个 DataFetcher
用来请求数据,这里的请求是对数据源的请求,不仅仅限于网络请求。
1 | public interface ModelLoader<T, Y> { |
ModelLoader
的类型很多,他们统一被 GenericLoaderFactory
管理,使用的工厂模式,在使用时使用对应的工厂类创建,同时加入缓存,获取时优先从缓存中获取,他是 Glide
的成员,在 Glide
初始化时被创建,并注册常用的 ModelLoaderFactory
,这个类主要做两件事情:
- 管理
ModelLoader
工厂的注册表 - 管理
ModelLoader
的缓存
注册表和缓存使用两层 Map
结构实现,理解起来比较麻烦,例如注册表表的定义如下:
1 | Map<Class/*T*/, Map<Class/*Y*/, ModelLoaderFactory/*T, Y*/>> |
所以 String->InputStream
和 String->ParcelFileDescriptor
的 ModelLoaderFactory
注册进去是:
1 | Map<String,Map<InputStream, ModelLoaderFactory<String,InputStream>>> |
这样一个结构的注册表,当你拿着 String->InputStream
去查找时,就能找到对应的 Factory
。
1 | GenericLoaderFactory.java |
缓存就不必说了,每次使用工厂创建耗时耗力,在内存中缓存一份,等查找时优先去内存中查找已经创建过的 ModelLoader
直接复用就可以了。
1 | GenericLoaderFactory.java |
最后,这个注册表是可扩展的,你可以给他注册自己的 ModelLoaderFactory
来解析合适的数据类型,当然针对常用的类型,Glide
类中已经注册了默认的许多工厂。
1 | register(File.class, ParcelFileDescriptor.class, new FileDescriptorFileLoader.Factory()); |
这样理解起来稍微有些抽象,我们来看一个网络相关的注册实现,GlideUrl
是一个包装,所有和网络相关的都会最终转成 GlideUrl
:
1 | register(GlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory()); |
看一下 HttpUrlGlideUrlLoader
的实现,实现一个方法 getResourceFetcher()
,返回 HttpUrlFetcher
这个类主要用来使用 HttpUrlConnection
发起网络请求,请求图片返回 InputStream
流。
1 | public class HttpUrlGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> { |
总结:可以发现 ModelLoader
主要就是用来对源数据进行一个请求操作。
DataLoadProvider
DataLoadProvider
负责编解码操作,主要是提供编解码器,将 InputStream
和 File
转换成真正意义上的图片,也就是 Bitmap
和 GifDrawable
等。
1 | public interface DataLoadProvider<T, Z> { |
上面的接口使用了很多范型,我们借助 StreamBitmapDataLoadProvider
来理解他,这个类是一个实现,它是针对 InputStream
和 Bitmap
编解码的实现
1 | public class StreamBitmapDataLoadProvider implements DataLoadProvider<InputStream, Bitmap> { |
也是采用注册表的方式,但是没有使用工厂,直接存储在内存中,使用 DataLoadProviderRegistry
来管理,内部同样是一个 Map
结构,存放了需要使用 DataLoadProvider
,而且他也是 Glide
的成员对象。
1 | public class DataLoadProviderRegistry { |
在 Glide
创建的时候初始化
1 | Glide.java |
Transcoder
Transcoder
做的是一个转码操作,负责将一种资源转换成另一种资源。
1 | public interface ResourceTranscoder<Z, R> { |
使用 TranscoderRegistry
管理,注册表管理。1
2
3
4
5
6public class TranscoderRegistry {
private static final MultiClassKey GET_KEY = new MultiClassKey();
private final Map<MultiClassKey, ResourceTranscoder<?, ?>> factories =
new HashMap<MultiClassKey, ResourceTranscoder<?, ?>>();
}
同样是 Glide
的成员,在 Glide
创建的时候注册
1 | Glide.java |
对比
类 | 管理类 | 原始数据类型 | 目标数据类型 |
---|---|---|---|
ModelLoader | GenericLoaderFactory | String(网络,文件等) int(资源) Uri(资源定位) File(文件) URL(网络) |
InputStream ParcelFileDescriptor |
DataLoadProvider | DataLoadProviderRegistry | ParcelFileDescriptor InputStream |
Bitmap GifDrawable |
ResourceTranscoder | TranscoderRegistry | Bitmap GifBitmapWrapper |
GlideDrawable GlideBitmapDrawable |
总结
我们就以加载文件里的图片为例完整的理解一下第一次的基本加载过程:
1 | 数据源 |
对于上述加载过程构造的 RequestBuilder
为
1 | public class GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> {} |
可以看出
1 | File -> InputStream -> Bitmap -> GlideDrawable |
理解了这几种转换就能更清晰的理解 Glide
的内部原理
目前维护的几个项目,求 ✨✨✨✨
- SocialSdk 登录分享功能原生接入
- LightAdapter 轻量级适配器
- ImageEditor 图片处理,裁剪旋转,贴纸涂鸦,滤镜等
- WeexCube Weex 容器方案
- Kotlin 学习系列总结,共计 22 篇
- 本文链接: http://cdevlab.top/article/47636f69/
- 版权声明: 版权所有,转载请注明出处!