Glide
Glide加载图片流程
Glide.with(context).load(String).into(xx)
这一行代码干了多少事,其完整的流程:
With()
Glide.with(context) 生成感知生命周期的requestManager;
1 | 1 with(context) |
Load()
RequestManager.load()确定解码类型并构建requestBuilder
1 | 2 load(string)多个重载,以String为例(没有手动调用asXXX()的情况下) |
Into()
RequestBuilder.into()确定transformations、构建ViewTarget、主线程Handler的Executor后,构建SingleRequest对象,将request对象保存到ViewTarget中,之后调用requestManager.track(target, request)中执行request.begin()开始正式加载流程。
Into(ImageView)
1 | 3 into(ImageView) 多个重载,以ImageView为例 |
Engine.load()
1 | 4 engine.load() |
Generator具体执行流程
在DecodeJob的getNextGenerator被执行到时,Generator会被初始化,初始化时,Generator会通过调用decodeHelper.getCacheKeys -> decodeHelper.getLoadData -> glideContext.getRegistry().getModelLoaders(model);
对 MultiModelLoaderFactory 中对 Register 注册表中已注册的所有entries的遍历,寻找与model类型匹配的Entry(包含modelClass、dataClass、具体loader的factory类)。
//注:此处的Register中可以是自己注册 model -> factory 的映射,也可能是Glide默认注册的那些。默认的model为String,启用okhttp的情况下映射到的是 OkHttpUrlLoader 的factory类。最后走OkHttpStreamFetcher请求网络
//注: String类型的model 在 StringLoader 中会先被解析成Uri,然后包装成 GlideUrl 类然后重新寻找对应的loader,也就是OkHttpUrlLoader
//注: 注册表 Register中,注册时 Key 为model的类型,value为 ModelLoaderFactory,ModelLoaderFactory build -> ModelLoader buildLoadData -> LoadData
以磁盘缓存模式为DATA_CACHE为例:
generator的执行顺序是 ResourceCacheGenerator(若有) -> DataCacheGenerator(若有) -> SourceGenerator(若不存在磁盘缓存)
第一次 decodejob 执行是在 diskCacheExecutor 线程池中
此时,首次进入runWrapped -> case INITIALIZE -> runGenerators() 时,执行的是 DataCacheGenerator.startNext(),由于此时磁盘缓存还不存在(所以也没法helper.getModelLoaders(cacheFile)),startNext返回false,走到下一个stage和generator,也就是 stageStage.SOURCE 并currentGenerator为SourceGenerator,当 stage == stageStage.SOURCE ,走到decodeJob的 reschedule(RunReason.SWITCH_TO_SOURCE_SERVICE),此时decodejob会被加入到 sourceExecutor 线程池中重新执行。
第二次 decodejob 执行是在 sourceExecutor 线程池中
此时,重新进入runWrapped -> case SWITCH_TO_SOURCE_SERVICE -> runGenerators() 时,执行的是 SourceGenerator.startNext(),遍历当前符合的所有 ModelLoader.LoadData(其实就是一个,fetcher为OkHttpStreamFetcher的LoadData对象),最后执行loadData.fetcher.loadData() 走到 OkHttpStreamFetcher.loadData() 调用okhttp下载数据,并监听 onDataReady() 和 onLoadFaild() 回调。
onDataReady()下载完成后,数据赋值给 SourceGenerator.dataToCache ,之后又 reschedule(RunReason.SWITCH_TO_SOURCE_SERVICE) 到sourceExecutor线程池中重新执行。
第三次 decodejob 执行依然是在 sourceExecutor 线程池中
此时,依然是runWrapped -> case SWITCH_TO_SOURCE_SERVICE -> runGenerators() ,SourceGenerator.startNext(),由于此时dataToCache不为空,走 SourceGenerator.startNext() 中先Encode数据然后存储磁盘缓存数据的逻辑,即调用 SourceGenerator.cacheData() 将源数据通过decodeHelper.getDiskCache.put(key = new DataCacheKey(loadData.sourceKey, helper.getSignature(), value = new DataCacheWriter<>(encoder, data, helper.getOptions()))写入磁盘缓存。同时构建一个 DataCacheGenerator执行其 startNext()。
DataCacheGenerator.startNext()中,此时由于已经写过磁盘缓存了,cacheFile不为空,于是走到helper.getModelLoaders(cacheFile)得到缓存文件的读取modelLoaders(此时有四个ByteBufferFileLoader,FileLoader$StreamFactory、 FileLoader$FileDescriptorFactory、UnitModelLoader)
根据当前的resourceClass和transcodeClass,以及loadData的dataClass(ByteBuffer)确定为由ByteBufferFileLoader处理,执行ByteBufferFileLoader.loadData。之后借助ByteBufferUtil类随机读取的方式,从磁盘文件中读取源数据的ByteBuffer,之后数据回调到 DecodeJob.onDataFetcherReady(),然后执行 decodeFromRetrievedData()解码源数据 (注,此时还在sourceExecutor线程池中)。
通过 这一串调用之后,走到遍历 decodePaths 尝试解析的步骤,此时一般有(AnimatedImageDecoder, ByteBufferGifDecoder, BitmapDrawableDecoder以及自定义的比如AvifBufferBitmapDecoder),任何一个decoder解码成功后即结束这段逻辑(decode时会执行Downsampler类的逻辑进行采样缩放并且解码),
重新走到 DecodeJob: decodeFromRetrievedData() -> notifyEncodeAndRelease() -> notifyComplete() ->
EngineJob: onResourceReady() -> notifyCallbacksOfResult() -> onEngineJobComplete()
此处执行活动缓存的写入逻辑,activeResources.activate(key, resource); 之后就是资源回收任务结束移除之类的收尾工作。CallResourceReady.run() -> engineResource.acquire();callCallbackOnResourceReady(cb); -> SingleRequest.onResourceReady() -> ImageViewTarget.onResourceReady() -> ImageViewTarget.setResourceInternal() -> DrawableImageViewTarget.setResource() -> view.setImageDrawable();
此处是将解码后的图片资源直接通过view.setImageDrawable显示出来。
Tips
- Glide是先将图片下载到磁盘,然后再从磁盘中读取。之后decode使用
- 不存在磁盘缓存需要fetch的情况下:存储和读取磁盘数据,包括后续的磁盘取出后解码操作 是在sourceExecutor线程池中进行的(即是都在上述的第三步中完成)
- 存在磁盘缓存的情况下:读取磁盘数据和解码数据 是在diskCacheExecutor线程池中进行的。
整体流程参考:
Glide缓存
三级缓存:
磁盘(DiskLruCache) -> LRUCache(不活跃资源) -> ActiveResources(使用中资源WeakReference)
内存缓存分为 ActiveResources弱引用 的和 LruCache ,其中正在使用的图片使用弱引用缓存,暂时不使用的图片用 LruCache缓存,这一点是通过 图片引用计数器(acquired变量)来实现的
内存缓存
内存缓存的key是由model, signature, width, height, transformations, resourceClass, transcodeClass, options
等因素共同组成的,因此,不同的宽高、变换之类的因素会生成不同的内存缓存。
活动缓存
活动缓存(activeResources: Map<Key, ResourceWeakReference>)
key为EngineKey对象,value为弱引用的图片缓存ResourceWeakReference实例
正在使用的图片缓存(被ImageView或Activity引用的),在gc或所在Activity(fragment宿主activity)销毁时被移到内存缓存
(为什么这一层缓存要用弱引用缓存的方式:一个是这一层缓存因为没有限制缓存大小,用弱引用如果遇到gc即可降级;一个是)
- 资源被包装成带引用计数的EngineResource,标记引用资源的次数(当引用数acquired不为0时阻止被回收或降级,降级即是存储到LruCache中)
- 这一级缓存没有大小限制,所以使用了资源的弱引用
- 存:每当下载资源后会在onEngineJobComplete()中存入ActiveResource,或者LruCache命中后,将资源从中LruCache移除并存入ActiveResource。
- 取:每当资源释放时,会降级到LruCache中(请求对应的context onDestroy了或者被gc了)
- 开一个后台线程,监听ReferenceQueue,不停地从中获取被gc的缓存,将其从ActiveResource中移除,并重新构建一个新资源将其降级为LruCache(这一步并不会影响页面展示,图片内存没有销毁,只是被降级到有大小限制的LruCache中)
- ActiveResource是为了缓解LruCache中缓存造成压力,因为LruCache中没有命中的缓存只有等到容量超限时才会被清除,强引用即使内存吃紧也不会被gc,现在当LruCache命中后移到ActiveResource,弱引用持有,当内存吃紧时能被回收。
活动缓存是感知生命周期的
当绑定的context销毁onDestory时,RequestManager会将该事件传递给RequestTracker,然后触发该请求Resource的clear,再调用Engine.release,将改activiteResource降级到LruCache
LRU缓存LRUCache
LRU缓存LRUCache(memoryCache: LruCache<Key, Resource<?>>)
key为EngineKey对象,value为图片缓存Resource实例
当图片不再显示时,图片会从活动缓存移到内存缓存。存储的是暂时不用的图片缓存(依靠图片引用计数器acquired变量实现,当acquired大于0时存在于活动缓存中,为0是移到内存缓存中)
使用 LinkedHashMap实现, 存储从活跃图片降级的资源,使用Lru算法淘汰最近最少使用的
Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
存:从活跃图片降级的资源(退出当前界面,或者ActiveResource资源被回收)
取:网络请求资源之前,从缓存中取,如没命中活动资源则取活动资源,没命中活动资源则取内存缓存LRUCache,若命中则直接从LruCache中移除了,没命中则走磁盘或网络。
内存缓存最大空间(maxSize) = 每个进程可用的最大内存(activityManager.getMemoryClass() * 0.4
(低配手机的话是: 每个进程可用的最大内存 * 0.33)
activityManager.getMemoryClass值:
- 低端设备可能在 16-32 MB 范围内。
- 中档设备通常在 64-128 MB 范围内。
- 高端设备和新款设备可能在 256 MB 或更高。//当设置largeHeap时,最多可申请512M
磁盘缓存(已证)
对于远程资源:DATA/AUTOMATIC 策略下的磁盘缓存的key只由url(和signature,但一般没有)决定。
RESOURCE 策略下的磁盘缓存的key决定因素还要包括 宽高、变换等多种因素
磁盘缓存(diskLruCache: LinkedHashMap<String, Entry>)
- 缓存Key Value:
key为 经过Sha256算法对 DataCacheKey(GlidUrl和signature)或ResourceCacheKey(GlideUrl、singnature、width、
height、transformation等多个因素) 摘要后 的64位字符串,value为File实例
- 在内存中用LinkedHashMap实现的LruCache记录一组Entry,Entry内部包含一组文件,文件名即是key,并且有开启后台线程执行删除文件操作以控制磁盘缓存大小。
- 写磁盘缓存即是触发Writer将数据写入磁盘,并在内存构建对应的File缓存在LinkedHashMap中
- 磁盘缓存的存储顺序记录在journal文件中,有时该文件过大会导致glide初始过慢。
- 磁盘缓存大小默认250MB,根据缓存策略的不同可能存储原始图片或解码后的图片。
DiskCacheStrategy.ALL : //表示既缓存原始图片,也缓存decode过后的图片。
DiskCacheStrategy.NONE: //表示不缓存任何内容。
DiskCacheStrategy.RESOURCE: //表示只缓存decode过后的图片,依然编码为jpeg/png等格式存储
DiskCacheStrategy.DATA: //表示只缓存原图片(decode前)。 如webp/avif
DiskCacheStrategy.AUTOMATIC //(默认选项)Remote资源下走DiskCacheStrategy.DATA,Local资源下走DiskCacheStrategy.RESOURCE。
磁盘-RESOURCE
实际是内存中解码后的Bitmap对象,编码成磁盘需要的文件格式,jpeg/png,之后再存磁盘
解码后的资源的磁盘缓存(经过转换,解压之后的体积较大,解码速度较快的,由bitmap直接编码成的JPEG/PNG等未压缩格式)
此时存储的文件是文件头为ffd8 ffe0 0010 4a46 4946 0001 0100 0001
的jpeg格式。
Resource 代表了解码后的资源,即已经从原始数据(如网络图片的字节流)解码并转换为可直接使用的数据形式,之后还是需要编码成文件格式jpeg/png等文件存储格式。Resource 封装了解码后的资源,并提供了一些管理功能,如引用计数和资源回收。
磁盘-DATA
原资源的直接数据缓存(未解码,转换,磁盘体积较小,解码速度较慢)(AVIF/WEBP等压缩格式)
此时存储的文件是文件头为0000 0020 6674 7970 6176 6966 0000 0000
的avif格式。
DataCache 是指数据缓存,主要用于缓存原始数据(例如,通过网络请求获取的图片字节流)。数据缓存可以加速后续的解码过程,因为原始数据已经被缓存下来,不需要再次从网络或其他源获取。
磁盘-AUTOMATIC
Remote资源下走DiskCacheStrategy.DATA,Local资源下走DiskCacheStrategy.RESOURCE。
Tips
对于远程的资源,DATA和AUTOMATIC是一样的。
想缩短解码时间可以考虑使用RESOURCE模式,本质上是用磁盘空间 换 解码时间:decode后的资源存储空间更大,但解码时间更短。
对于webp,avif等模式,RESOURCE缓存的是.jpeg格式,其内容是decode后内存中使用的bitmap对象直接,不带压缩的二进制内容。DATA缓存的则是原始的图片格式
**那么对于.jpg文件,RESOURCE跟DATA模式的区别是啥:jpg本身也是有压缩的,RESOURCE模式下存储的文件格式仍为jpg,但是其内容是decode后的,尺寸更大而解码时间更短 **
那么对于.gif文件,不管什么模式都一样,就是缓存gif图原始文件
互联网早期图片现在看到很多事绿色且模糊,其实就是由于jpeg的压缩是有损的,每次存到磁盘中可能经过一次压缩,并且上传时会再压缩一次,重复多次后就会质量下降
Glide缓存Key
Glide生成key(两级内存缓存Key类型为同一个EngineKey对象,磁盘缓存Key类型为DataCacheKey或ResourceCacheKey摘要的哈希字符串)的方式涉及的参数有8种,其中都包括图片URL、签名。可能包括宽高、变换等
1 | EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, |
宽高一定是明确有值的。即使wrap_content也会用屏幕最长的一边作为兜底宽高。
Glide的缓存大小默认是多少
内存缓存大小
内存缓存最大空间(maxSize) = 每个进程可用的最大内存(activityManager.getMemoryClass() * 0.4
(低配手机的话是: 每个进程可用的最大内存 * 0.33)
activityManager.getMemoryClass值:
- 低端设备可能在 16-32 MB 范围内。
- 中档设备通常在 64-128 MB 范围内。
- 高端设备和新款设备可能在 256 MB 或更高。//当设置largeHeap时,最多可申请512M
磁盘缓存大小
磁盘缓存大小: 默认250MB
磁盘缓存目录: 项目/cache/image_manager_disk_cache
这里有个点:随着图片磁盘缓存的存取,由于每次存取磁盘图片glide都会将存取操作记录到日志文件(journal文件),日志文件会逐渐增大到可能几兆大小,导致glide初始化延迟(glide的初始化中,磁盘缓存初始化时会涉及到将日志文件读取到内存中操作)。
这里可以增加一个首页模块的磁盘缓存目录,比如项目/cache/image_manager_disk_home_cache
,启动时先初始化首页的磁盘缓存(现为10m)再初始化主体的磁盘缓存,避免日志文件过大导致的初始化时间太长阻塞App启动。
Other
感知生命周期
通过链式调用中with(xxx)传入的activity或Fragment,Glide实现了对所属activity或者fragment生命周期监听:通过new一个隐形的fragment(SupportRequestManagerFragment.class),嵌入到所要监听的fragment或者activity所在的Activity中,这样当宿主Acitivity的生命周期变化时,可以通过嵌入的fragment监听回调,并在RequestManager中执行:
**onStart(): 继续请求resumeRequests() **
onStop(): 暂停请求pauseRequests()
onDestory(): 销毁请求、对应页面的活动缓存降级到内存缓存LRUCache,移除监听等操作
// ps:当 Activity时,Glide 会清理相关资源,移除 Fragment 不会立即触发 ActiveResources 缓存降级,只有宿主 Activity 销毁时才会。
//com/bumptech/glide/request/target/CustomViewTarget.java 中保留了一个没打开的接口clearOnDetach(),可以实现当ImageView detachedFromWindow的时候释放图片缓存。但由于考虑到太激进的释放可能导致缓存复用效率
glide是怎么拿到的width,height(view未测绘前拿不到宽高)
如果view的布局宽高有值,或view本身的宽高有值,会直接返回布局宽高或view宽高。如果没有:
ViewTarget会为所持有的View注册view树绘制回调,待到经过测量布局之后回调onSizeReady()了,才会发起engine.load。(如果wrap_content则会用getMaxDisplayLength()屏幕长的一边作为宽高返回)
addOnPreDrawListener(OnPreDrawListener listener)
如果是Wrap_content?
那么会返回 屏幕高(长的一边) * 屏幕高,比如 1080 * 1920 屏幕会返回 1920 * 1920兜底,在后续流程比如内存缓存Key生成时候,也是用的这个兜底宽高。
具体见ViewTarget.getMaxDisplayLength()
感知内存吃紧
注册ComponentCallbacks2,实现细粒度内存管理:
- onLowMemory(){清除内存}
- onTrimMemory(){修剪内存}
1 | memoryCache.trimMemory(level); // 内存缓存 |
可以设置在onTrimMemory时,取消所有正在进行的请求。
Glide线程池
源线程池sourceExecutor
(触发下载时存取磁盘和解析使用的线程池,非下载线程池,下载包给okhttp/httpurlconnection了):
定长为 cpu数量(最大4)(核心线程和工作线程数量都最大为4)的线程池
1 | private GlideExecutor getActiveSourceExecutor() { |
磁盘缓存diskCacheExecutor
(从Resource或DataCache中加载图片)线程池:定长为1(核心线程和工作线程数量都为1)的线程池
//diskCacheExecutor
//DEFAULT_DISK_CACHE_EXECUTOR_THREADS = 1
public static GlideExecutor newDiskCacheExecutor() {
return newDiskCacheExecutor(
DEFAULT_DISK_CACHE_EXECUTOR_THREADS,
DEFAULT_DISK_CACHE_EXECUTOR_NAME,
UncaughtThrowableStrategy.DEFAULT);
}
加载Gif流程
读取流的前三个字节,若判断是gif,则会命中gif解码器-将资源解码成GifDrawable,它持有GifFrameLoader会将资源解码成一张张Bitmap并且传递给DelayTarget的对象,该对象每次资源加载完毕都会通过handler发送延迟消息回调 onFrameReady() 以触发GifDrawable.invalidataSelf()重绘。加载下一帧时会重新构建DelayTarget
QA
ActiveResources弱引用问题
Q: glide 当前页面的图片缓存是存在activiteResource的弱引用的,也就是说当gc时当前页面的图片缓存会被回收?那岂不是当前页面会空白
A: 不会啊,因为实际的资源被页面的view持有着,而ActiveResources只是一层弱引用而已。而已经不被view持有(没显示出来的图片)但仍在活动缓存中的弱引用会被降级到LRU内存缓存中。
活动资源缓存的存在以较小的代价减小Lru缓存的压力,提升Lru缓存的效率。
原因是活动资源缓存通过缓存的对象本身就是在内存中进行使用,缓存是只是建立一个弱引用关系。如果过没有活动资源缓存,每一次使用的资源都加入内存缓存,极有可能因为放入Lru缓存的数据过多,导致正在使用资源从Lru缓存中移除,等到下次来进行加载的时候因为没有对应的引用关系,找不到原来内存中正在使用的那个资源,从而需要再次从文件或者网络进行数据加载。这样同一份资源需要使用两处或者多处内存。大大的提高了内存消耗。总而言之,活动资源缓存以较小的代价提高了Lru缓存的使用效率,防止加载中的资源被lru回收。
字节数组的复用池
BitmapPool
- BitmatPool 是 Glide 维护了一个图片复用池,LruBitmapPool 使用 Lru 算法保留最近使用的尺寸的 Bitmap。
- api19 后使用bitmap的字节数和config作为key,而之前使用宽高和congif,所以19以后复用度更高
- 用类似LinkedHashMap存储,键值对中的值是一组Bitmap,相同字节数的Bitmap 存在一个List中(这样设计的目的是,将Lru策略运用在Bitmap大小上,而不是单个Bitmap上),控制BitmapPool大小通过删除数据组中最后一个Bitmap。
- BitmapPool 大部分用于Bitmap变换和gif加载时
ArrayPool
- 是一个采用Lru策略的数组池,用于解码时候的字节数组的复用。
- 清理内存意味着清理MemoryCache,BitmapPool,ArrayPool
相关源码
load可加载多种model的图片
1 | RequestBuilder.java |
获得View宽高
1 | SingleRequest.java |
内存缓存Key的生成
1 | Engine.java |
请求缓存流程
1 | Engine.java { |
相关概念
Resource 代表了解码后的资源,即已经从原始数据(如网络图片的字节流,一般为AVIF、WEBP)解码并转换为可直接使用的数据形式(如 Bitmap、Drawable 等,如果存到磁盘会encode成jpeg/png格式)。Resource 封装了解码后的资源,并提供了一些管理功能,如引用计数和资源回收。
DataCache 是指数据缓存,主要用于缓存原始数据(例如,通过网络请求获取的图片字节流)。数据缓存可以加速后续的解码过程,因为原始数据已经被缓存下来,不需要再次从网络或其他源获取。
Source 代表数据的源头,即原始数据的来源。常见的源包括网络(主要)、文件系统、资源文件、内容提供者等。
网络源:从 URL 或网络地址加载数据。
文件源:从本地文件系统加载数据。
资源源:从应用的资源文件中加载数据。
内容提供者:通过内容提供者(ContentProvider)加载数据。
- URL:主要用于标识网络上的资源。示例:https://example.com/image.jpg
- URI:是一个更广泛的概念,可以标识任意一种资源,包括本地文件和网络资源。示例:file:///storage/emulated/0/Download/image.jpg 和 content://media/external/images/media/12345
resourceClass //图片以什么类型解码图片资源
resourceClass 是指图像加载过程中,Glide 从源(如网络或本地存储)加载并解码后的资源类型。它表示 Glide 获取图像数据后将其解码成什么类型的资源。
常见的 resourceClass 类型包括:
- Bitmap:用于静态图像,如 JPEG、PNG。
- GifDrawable:用于 GIF 动图。
- Drawable:一个通用类型,可以表示 BitmapDrawable、GifDrawable 等。
指定 resourceClass 可以确保 Glide 以指定的方式解码和处理图像资源。例如:
1
2
3
4
5
6 java
Copy
Glide.with(context)
.asBitmap() // 指定resourceClass为 Bitmap。不指定时默认asDrawable为drawable
.load("https://example.com/image.jpg")
.into(imageView);transcodeClass // 最终提供给 Target(如 ImageView)的数据类型
transcodeClass 是指从 resourceClass 类型转换后的最终数据类型,它表示图像加载和解码后,最终提供给 Target(如 ImageView)的数据类型。
常见的 transcodeClass 类型包括:
- Drawable:Glide 默认使用 Drawable 作为最终显示的类型,因为它可以表示不同类型的图像资源。
- Bitmap:如果你需要直接处理 Bitmap 对象,可以指定 transcodeClass 为 Bitmap。
- 自定义类型:你可以定义自己的转换器,将资源转换为自定义类型。
通过自定义转换器(ResourceTranscoder),你可以将 resourceClass 类型转换为所需的 transcodeClass 类型。例如,如果你想将 GIF 转换为静态图像或者其他类型,可以使用自定义转换器。
总结
- resourceClass:表示 Glide 从源加载并解码后的资源类型(如 Bitmap、GifDrawable)。
- transcodeClass:表示从 resourceClass 类型转换后的最终数据类型,可以是 Drawable、Bitmap 或其他自定义类型。