编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.LeakCanary设计思想
  • 02.Glide图片加载设计
  • 03.Gson序列化方案设计
  • 04.ARouter路由实践设计
  • 05.EventBus事件设计
  • 07.OkHttp网络请求设计
  • 08.gRPC框架设计思想

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
贡献者: yangchong211
上一篇
01.LeakCanary设计思想
下一篇
03.Gson序列化方案设计