02.Glide图片加载设计
目录介绍
- 01.整体概述介绍
- 1.1 项目背景介绍
- 1.2 问题和思考
- 1.3 设计核心目标
- 1.4 产生收益
- 02.Glide设计思路
- 2.1 整体设计思路
- 2.2 设计初始化思路
- 2.3 封装参数设计
- 2.4 解析路径设计
- 2.5 读取资源设计
- 2.6 缓存方案设计
- 2.7 图片解码和压缩设计
- 2.8 图片显示设计
- 2.9 其他一些设计
- 03.Glide原理思考
- 3.1 要思考一些问题
- 3.2 原理流程的概括
- 3.3 封装参数实践
- 3.4 解析路径实践
- 3.5 读取资源实践
- 3.6 缓存方案实践
- 3.7 图片解码和压缩实践
- 3.8 图片显示实践
- 04.一些技术点思考
- 4.1 为何监听生命周期
- 4.4 对象池的优化思考
- 4.5 缓存方案思考
- 4.6 加载进度思考
- 4.9 Glide一些常见优化
- 05.Glide优秀的设计
- 5.1
- 06.如何实现加载速度
- 6.1 如何实现加载速度
- 6.2 加载速度思路分析
- 6.3 替换通信组件
- 6.4 添加拦截器和监听
- 6.5 回调和计算加载速度
01.整体概述介绍
1.1 项目背景介绍
1.2 问题和思考
- Glide一些常见的问题
- Bitmap的使用过程中都有哪些常见问题,它如何占用内存,甚至导致OOM,那么Glide是如何做内存优化的?
- 如何使用Glide加载本地图片或资源文件,如何加载网络图片?如何让你设计,你会怎么做?
- 如何处理Glide加载图片时的占位符和错误情况?如何加载圆形或圆角图片?如何使用Glide加载GIF动画?
- Glide的图片加载过程是如何进行的?可以详细描述一下吗?图片缓存机制是怎样的?有哪些类型的缓存?
- 如何使用Glide加载大量图片时进行分页加载或分批加载?如何根据网络状况动态调整图片质量?
- 高级一点的问题
- Glide是否支持图片的渐进式加载?如何做到图片的缩略图加载?Glide是否支持图片的预加载和预解码?
- Glide的生命周期是如何管理的?如何在Activity或Fragment销毁时取消图片加载请求?
- Glide是否支持图片的动态变换,如旋转、缩放或模糊效果?图片的缩放和裁剪操作如何实现?
- 如何使用Glide加载视频缩略图?Glide是否支持图片的缩略图预览?如何实现缩略图预览?
- Glide是否支持图片的优先级加载?如何使用Glide加载图片时实现渐进式加载效果?如何实现模糊效果?
- Glide是否支持图片的加载进度监听?针对加载高清大图,Glide是如何做的,进度监听设计思路是什么?
- Glide是否支持图片的多图像请求,如加载多个图片并合成为一个图片?
- Glide是否支持图片的缩略图预览,如在列表中显示低分辨率的缩略图?如何使用Glide加载图片时实现图片的水印效果?
- Glide是否支持图片的自定义变换和过渡动画?如何实现自定义变换和过渡动画?
- 性能相关的问题
- 如何使用Glide加载大型图片或高分辨率图片时避免内存溢出?
- Glide的缓存机制是怎样的?Glide的图片加载过程中如何处理图片的内存缓存和磁盘缓存?
1.3 设计核心目标
- 目前图片加载框架有好几个,那么如果让你来设计Glide图片加载框架,你有那些核心目标呢?
- 高效的图片加载:Glide旨在提供高效、快速的图片加载体验。可以采用了多种优化策略,如内存和磁盘缓存、图片重用、请求优先级管理等。它能够有效地管理图片加载过程中的资源和内存,以提供流畅的用户体验。
- 简化的使用方式:Glide的设计目标之一是提供简单易用的API,使开发者能够轻松地集成和使用。通过简洁的链式调用和自动化的图片加载过程,开发者可以快速地实现图片加载功能,无需过多的配置和复杂的代码。
- 灵活的配置选项:Glide提供了简化的使用方式,但它也提供了丰富的配置选项,以满足不同场景下的需求。开发者可以根据具体需求来配置图片加载的缓存策略、图片变换、请求参数等,以实现个性化的图片加载效果。
- 强大的扩展性:Glide的设计允许开发者通过自定义组件和扩展来满足特定的需求。开发者可以自定义图片加载过程中的各个环节,如网络请求、图片解码、缓存策略等,以实现更高级的功能和定制化的需求。
1.4 产生收益
02.Glide设计思路
2.1 整体设计思路
- 大多数图片框架加载流程
- 概括来说,图片加载包含封装,解析,下载,解码,变换,缓存,显示等操作。
- 图片框架是如何设计的
- 封装参数:从指定来源,到输出结果,中间可能经历很多流程,所以第一件事就是封装参数,这些参数会贯穿整个过程;
- 解析路径:图片的来源有多种,格式也不尽相同,需要规范化;比如glide可以加载file,io,id,网络等各种图片资源
- 读取缓存:为了减少计算,通常都会做缓存;同样的请求,从缓存中取图片(Bitmap)即可;
- 查找文件/下载文件:如果是本地的文件,直接解码即可;如果是网络图片,需要先下载;比如glide这块是发起一个请求
- 解码:这一步是整个过程中最复杂的步骤之一,有不少细节;比如glide中解析图片数据源,旋转方向,图片头等信息
- 变换和压缩:解码出Bitmap之后,可能还需要做一些变换处理(圆角,滤镜等),还要做图片压缩;
- 缓存:得到最终bitmap之后,可以缓存起来,以便下次请求时直接取结果;比如glide用到三级缓存
- 显示:显示结果,可能需要做些动画(淡入动画,crossFade等);比如glide设置显示的时候可以添加动画效果
2.2 设计初始化思路
2.3 封装参数设计
2.4 解析路径设计
2.5 读取资源设计
2.6 缓存方案设计
2.7 图片解码和压缩设计
- 先说一个场景
- 加载图片到ImageView上。合理点的做法是「根据目标ImageView的尺寸,让解码器对原始图像进行下采样,以提供一个较低分辨率版本的缩略图」。
- 大概的思路:使用降采样使得图像符合显示区域的大小,生成对应图像的缩略图,并且进行解码,质量压缩等一系列操作压缩图片。
- Android的普通方案
- 采样率压缩+质量压缩:计算采样率从而减小图片的宽高,这里面加载需要注意inJustDecodeBounds参数设置。
- Glide的极致方案
- 同样会根据目标控件的尺寸,对图片进行适当的下采样、裁剪和变换,以减少内存占用,并确保加载过程尽快完成。
2.8 图片显示设计
2.9 其他一些设计
04.一些技术点思考
4.1 为何监听生命周期
- with()绑定生命周期
- with(Context context). 使用Application上下文,Glide请求将不受Activity/Fragment生命周期控制。
- with(Activity activity). 使用Activity作为上下文,Glide的请求会受到Activity生命周期控制。
- with(FragmentActivity activity). Glide的请求会受到FragmentActivity生命周期控制。
- with(android.app.Fragment fragment). Glide的请求会受到Fragment 生命周期控制。
- with(android.support.v4.app.Fragment fragment). Glide的请求会受到Fragment生命周期控制。
- 为何要绑定生命周期
- with()方法可以接收Context、Activity或者Fragment类型的参数。也就是说我们选择的范围非常广,不管是在Activity还是Fragment中调用with()方法,都可以直接传this。
- 那如果调用的地方既不在Activity中也不在Fragment中呢?也没关系,可以获取当前应用程序的ApplicationContext,传入到with()方法当中。
- 注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。
- 如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。
- Glide是如何解决图片加载生命周期的?(也是bug高发地带)
- 当一个界面离开之后,我们更希望当前的图片取消加载,那么 Glide 是怎么做到的呢?
- Glide 的使用方式上,一定需要传入一个 context 给它。它为什么需要拿上下文呢?原因就是可以根据不同的上下文进行处理,拿到 context (除了application context)之后,Glide做了一件很巧妙的事情,就是在这个界面上追加一个 fragment,由于 fragment 添加到了 activity 上,是可以捕获到生命周期的,因此可以在 destroy 的时候取消掉当前context下的 glide对象中的加载任务。
- 为什么标题后面说是 ‘也是bug高发地带’ 呢? 因为从实现方式上,它是巧妙的利用了fragment的生命周期来实现的‘销毁’动作,那么就类似于另外一个高发bug,延时的匿名内部类(网络请求callback回来),界面已经销毁,所以当前activity依附的glide也就销毁了的,此时再尝试加载图片的话,就会crash。
4.4 对象池的优化思考
- 说一下业务背景:Glide频繁请求图片
- 比如Glide中,每个图片请求任务,都需要用到类。若每次都需要重新new这些类,并不是很合适。而且在大量图片请求时,频繁创建和销毁这些类,可能会导致内存抖动,影响性能。
- 使用缓存池优化对象频繁创建
- Glide使用对象池的机制,对这种频繁需要创建和销毁的对象保存在一个对象池中。每次用到该对象时,就取对象池空闲的对象,并对它进行初始化操作,从而提高框架的性能。
- 多条件key缓存bitmap场景优化
- 第一步:我们需要定义一个Key对象来包含各种缓存的条件,例如我们除了图片名字作为条件,还有图片的宽度,高度也决定了是否是同一个资源。
- 第二步:支持多条件的缓存键值,但是每次查找缓存前都需要创建一个新的 Key 对象,虽然这个 Key 对象很轻量,但是终归觉得不优雅。glide源码中会提供一个 BitmapPool 来获取 Bitmap 以避免 Bitmap 的频繁申请。
- 第三步:在Map集合(key,bitmap)查找Key时,如果没有发现命中的值,那么就会创建新的值,并将其连同 Key 保存在 HashMap 中,不会对 Key 进行复用。而如果发现了命中的值,也就是说 HashMap 中已经有一个和当前 Key 相同的 Key 对象了,那么 Key 就可以通过 offer 方法回收到了 KeyPool 中,以待下一次查找时复用。
- 为何要多条件key
- 针对bitmap,加载图片特别频繁且多,不建议只是简单通过一个name图片名称作为键,因为可能图片名称是一样的,比如有时候接口返回同样名称的图片有大图,正常图,缩略图等,那样可能会存储重复或者碰撞。但是通过name,还有图片宽高字段,就可以大大减小这种问题呢。
4.9 Glide一些常见优化
06.如何实现加载速度
6.1 如何实现加载速度
6.2 加载速度思路分析
- 先来说一下业务背景
- 使用Glide来加载图片是非常简单的,但是让人头疼的是,却无从得知当前图片的下载进度。那么能否知道图片的下载速度就很重要呢。
- 如果这张图片很小的话,反正很快就会被加载出来。但如果这是一张比较大的图,用户耐心等了很久结果图片还没显示出来,这个时候你就会觉得下载进度功能是十分有必要的。
- 设计加载速度分析思路
- Glide内部HTTP通讯组件的底层实现是基于HttpUrlConnection来进行定制的。但是HttpUrlConnection的可扩展性比较有限,在它的基础之上无法实现监听下载进度的功能。
- 因此可以将Glide中HTTP通讯替换成OkHttp,利用OkHttp强大的拦截器机制,通过向OkHttp中添加一个自定义的拦截器,就可以在拦截器中捕获到整个HTTP的通讯过程,然后加入一些自己的逻辑来计算下载进度,这样就可以实现下载进度监听的功能。
6.3 替换通信组件
- 新建一个OkHttpFetcher类,并且实现DataFetcher接口。
- 这个主要是
- 然后新建一个OkHttpGlideUrlLoader类,并且实现ModelLoader
- 在这个里面,创建OkHttpClient对象,
- 新建一个ImageGlideModule类并实现GlideModule接口
- 在registerComponents()方法中将我们刚刚创建的OkHttpGlideUrlLoader和OkHttpFetcher注册到Glide当中,将原来的HTTP通讯组件给替换掉
//将原来的HTTP通讯组件给替换掉 OkHttpUrlLoader.Factory factory = new OkHttpUrlLoader.Factory(); registry.replace(GlideUrl.class, InputStream.class, factory);
- 为了让Glide能够识别自定义的ImageGlideModule
- 还得在AndroidManifest.xml文件当中加入如下配置才行
6.4 添加拦截器和监听
- 思考一下问题
- 将HTTP通讯组件替换成OkHttp之后,我们又该如何去实现监听下载进度的功能呢?这就要依靠OkHttp强大的拦截器机制了。
- 做法如下所示
- 只要向OkHttp中添加一个自定义的拦截器,就可以在拦截器中捕获到整个HTTP的通讯过程,然后加入一些自己的逻辑来计算下载进度,这样就可以实现下载进度监听的功能了。
public class ProgressInterceptor implements Interceptor { @NotNull @Override public Response intercept(@NotNull Interceptor.Chain chain) throws IOException { //拦截到了OkHttp的请求,然后调用proceed()方法去处理这个请求,最终将服务器响应的Response返回。 Response response = chain.proceed(chain.request()); //定义ProgressResponseBody主要是做监听进度处理逻辑 ProgressResponseBody progressResponseBody = new ProgressResponseBody(request.url().toString(), response.body()); Response newResponse = response.newBuilder().body(progressResponseBody).build(); return newResponse.body(); } }
- 添加拦截器
- 创建了一个OkHttpClient.Builder,然后调用addInterceptor()方法将刚才创建的ProgressInterceptor添加进去,最后将构建出来的新OkHttpClient对象传入到OkHttpGlideUrlLoader.Factory中即可。
- 代码如下所示
- 在registerComponents()方法中将我们刚刚创建的OkHttpUrlLoader和OkHttpStreamFetcher注册到Glide当中
OkHttpClient.Builder builder = new OkHttpClient.Builder(); //添加拦截器 builder.addInterceptor(new ProgressInterceptor()); builder.build();
6.5 回调和计算加载速度
- 新建一个ProgressListener接口,用于作为进度监听回调的工具。在ProgressInterceptor中加入注册下载监听和取消注册下载监听的方法。
- 使用了一个Map来保存注册的监听器,Map的键是一个URL地址。可能会使用Glide同时加载很多张图片,而这种情况下,必须要能区分出来每个下载进度的回调到底是对应哪个图片URL地址的。
public class ProgressInterceptor implements Interceptor { static final Map<String, ProgressListener> LISTENER_MAP = new HashMap<>(); public static void addListener(String url, ProgressListener listener) { LISTENER_MAP.put(url, listener); } public static void removeListener(String url) { LISTENER_MAP.remove(url); } }
- 定义了一个ProgressResponseBody
- 该构造方法中要求传入一个url参数和一个ResponseBody参数。那么很显然,url参数就是图片的url地址了,而ResponseBody参数则是OkHttp拦截到的原始的ResponseBody对象。
- 调用了ProgressInterceptor中的LISTENER_MAP来去获取该url对应的监听器回调对象,有了这个对象,待会就可以回调计算出来的下载进度了。
- 重写ResponseBody几个方法说明
- 重写contentType()、contentLength()和source()这三个方法,我们在contentType()和contentLength()方法中直接就调用传入的原始ResponseBody的contentType()和contentLength()方法即可,这相当于一种委托模式。
- 但是在source()方法中,就必须加入点自己的逻辑了,因为这里要涉及到具体的下载进度计算。
- source()方法返回处理过的bufferedSource
- 调用了原始ResponseBody的source()方法来去获取Source对象,接下来将这个Source对象封装到了一个ProgressSource对象当中,最终再用Okio的buffer()方法封装成BufferedSource对象返回。
- 这个ProgressSource是什么呢?
- 自定义的继承自ForwardingSource的实现类。只是负责将传入的原始Source对象进行中转。可以在中转的过程中加入自己的逻辑了。
- 在ProgressSource中我们重写了read()方法,然后在read()方法中获取该次读取到的字节数以及下载文件的总字节数,并进行一些简单的数学计算就能算出当前的下载进度了。
Bitmap缓存清除
由于 Bitmap 使用了 Finalizer 机制或引用机制来辅助回收,所以当 Java Bitmap 对象被垃圾回收时,也会顺带回收 Native 内存。出于这个原因,网上有观点认为 Bitmap 已经没有必要主动调用 recycle() 方法了,甚至还说是 Google 建议的。
Google 这番话确实是有误导性, not need to be called 确实是不需要 / 不必要的意思。抛开这个字眼,我认为 Google 的意思是想说明有兜底策略的存在,如果开发者没有调用 recycle() 方法,也不必担心内存泄漏。如果开发者主动调用 recycle() 方法,则可以获得 advanced 更好的性能 。
举个例子,Glide 内部的 Bitmap 缓存池在清除缓存时,会主动调用 recycle() 吗?看源码:
//LruBitmapPool.java
// 已简化
private synchronized void trimToSize(long size) {
while (currentSize > size) {
final Bitmap removed = strategy.removeLast();
currentSize -= strategy.getSize(removed);
// 主动调用 recycle()
removed.recycle();
}
}
参考博客
- Glide你为何如此秀?
- https://mp.weixin.qq.com/s/pAazRD9NaLPvBseG51ZOLw
- https://juejin.cn/post/6844904049595121672
- 为了性能,Glide 做了哪些优化?
- https://mp.weixin.qq.com/s/K73COuVeG5nfhO1ViVETUw