最初阅读 Glide v3 的源码时,感觉 Glide 的源码比我想象中复杂得多。复杂主要是因为有些对象的调用链很长,从初始化到实际使用中间隔了很多层。并且为了高扩展性,Glide 使用了大量接口,并根据需求分配不同的实现类。想要找到实际的实现类,确实要费点功夫。后来 Glide v4 的源码改进了不少,类的继承关系和整体结构简化了一些,阅读起来更清晰。另外,v4 的缓存逻辑也有变更。因此,下面的原理分析都是基于Glide v4 版本。

在阅读源码时,我总是提醒自己,源码是手段而不是目的,我们最终要关注的是 Glide 有哪些特性,它又是如何实现的。搞清楚了这些,源码的细节就不再重要了。当走完源码,你会发现,我们实际需要关注的逻辑其实很简单。

Glide 的源码挺多的,所以打算分为两篇,一篇在宏观上对 Glide 的几个要点的原理进行分析,一篇作为源码导读,详细走一遍源码。下面先了解一下 Glide 的特性:

Glide 的特性

  • Activity 结束时自动停止加载
  • 根据 ImageView 的宽高设置图片的宽高,也可以指定宽高
  • 可以设置缩略图和占位图
  • 实现了内存到文件的三级缓存

除了上面这些特性的实现原理外,我们还需要搞懂下面这些问题:

  • 每个图片请求生成 Key 的策略是什么?这决定了最终的缓存规则
  • 图片如何加载,加载的线程如何管理?

加载步骤

下面我们来看一下 Glide 是如何解决这些问题的,以我们最常见的 url 加载为例:

1
2
3
4
5
6
7
8
RequestOptions options = new RequestOptions()
.centerCrop()
.placeholder(R.drawable.placeholder);

Glide.with(context)
.load(url)
.apply(options)
.into(imageView);

一次图片加载主要分四步:

  1. 调用 with 方法根据 context 生成 RequestManager。实际上可以大致分为 Activity 和 Application 的 Manager 两种。前者可以在 Activity 销毁时自动停止加载;
  2. 调用 load 方法生成一个 RequestBuilder 用于构建 Request。所有必要的配置项和加载器都保存在 RequestBuilder 中。
  3. 调用配置方法,设置一些配置项;
  4. 调用 into 方法生成一个 Request,确定图片的大小,然后调用 Request 的 begin 方法开始加载图片并显示到 ImageView 中。

如何在 Activtiy 结束时停止加载

这个在 with 方法中就实现了。原理如下:

  1. 给 Activity 中添加一个空的 Fragment;
  2. 这个 Fragment 在构造时会新建一个 ActivityFragmentLifecycle 对象,并在自己的生命周期中回调 Lifecycle 对应的方法;
  3. Lifecycle 对应的方法中又会调用监听器中相应方法;
  4. RequestManager 实现了这个监听器,并在构造时将自己添加进 Lifecycle 的监听器列表中;
  5. 这样 Activity 被销毁时,RequestManager 的 onDestroy 方法会被调用。在此方法中清除请求,做一些销毁工作。

图片的宽高如何确定

在 SingleRequest 的 begin 方法中,会在加载之前进行图片大小的设置。

  • 如果用户指定了宽高,使用用户指定的宽高
  • 如果未指定,获取 View 的宽高
  • 最后在加载前将宽高乘以 sizeMultiplier。这个数代表压缩比,由用户设置,必须介于 0 和 1 之间,默认为1。
  • 乘完之后取整,这就是图片的最终宽高了。

那获取 ImageView 的宽高是如何实现的呢?
这个很简单。getSize 方法在 ViewTarget 类中实现。先获取 view 的宽高,判断是否有效,如果有效则回调。如果无效,说明 View 还未测量好,此时给 View 的 ViewTreeObserver 设置一个 Listener,在 Listener 的 onPreDraw 中重新获取宽高,再次判断。

缩略图和占位图如何实现

占位图的实现很简单,在加载图片前直接设置即可。

而如果设置了缩略图,缩略图和完整的图会同时加载,不保证加载完毕的顺序。如果缩略图先加载完就先显示,完整的图加载完毕后再覆盖。如果完整的图先加载完,则清除缩略图的请求。这就是我们看到的图片先模糊再变清晰的效果,这可以保证我们最快看到图片。

缩略图的原理用一句话概括就是使用两个图片请求同时开始加载,其具体实现是这样的:

  • 如果设置了缩略图的 RequestBuilder 或者 thumbSizeMultiplier,那么原 RequestBuilder 构建出来的 Request 是 ThumbnailRequestCoordinator。其包装了两个 SingleRequest,分别代表缩略图的请求(thumbRequest)和完整的请求(fullRequest);
  • ThumbnailRequestCoordinator 启动时,同时启动 thumbRequest 和 fullRequest;
  • 两个 Request 加载完后的回调方法并未特殊处理,都会显示在 ImageView 中;
  • 如果 thumbRequest 先执行完了,就先显示。fullRequest 执行完后就会正常覆盖;
  • 如果 fullRequest 先执行完,在 ThumbnailRequestCoordinator 的 onRequestSuccess 中会将 thumbRequest 取消掉。

缓存如何实现

在介绍缓存前我们要先搞清楚几个概念。我们在显示图片时传入的是 url,file,资源 id 等,这些被统称为 Model。然后 DataFetcher 根据 Model 从网络或者磁盘中加载回资源,此时图片资源的大小和形状都未经过处理,这时候叫 Data。图片回来后还需要进行一些大小和圆角之类的处理,处理后的图片资源叫 Resource。

Glide 实现了 4 种缓存,内存和文件中各两种,查找缓存时按顺序查找,分别是:

  1. 活跃资源(Active Resources),代表内存正在被其他 View 显示的图片资源;
  2. 内存缓存(Memory cache),代表最近显示过并缓存在内存中的图片资源;
  3. 资源(Resource),代表文件缓存中经过转换的图片文件;
  4. 原始数据 (Data),代表文件缓存中未经转换的图片源文件;

下面来看看这四种缓存分别是如何实现的。

在内存缓存中,每个图片资源被包装成一个 EngineResource。里面实现了对资源的配置和释放。同时,还有一个引用计数器,每次被显示时计数器加一,被释放时计数器减一,只有当计数器归零了,图片资源才会被真正释放。
另外,内存中存储的都是经过转换后的 Resource,匹配时要图片的大小和转换方式都一样才能匹配。

活跃资源(Active Resources)

活跃资源代表有其他 View 也在展示这张图片。其将 EngineResource 放在一个弱引用中,然后保存在 HashMap 里。活跃资源占用内存的大小没有限制。每个最终被 View 显示的 EngineResource,都会存储到这个 HashMap 中。

内存缓存(Memory cache)

活跃资源中找不到时,会去内存缓存中查找。Glide 基于 LinkedHashMap 实现了一个 LruCache,其子类 LruResourceCache 就是内存缓存的容器,缓存的最大容量在 Glide 初始化时通过计算设置好。每次添加资源时检查资源总大小,如果超过了大小限制,就会回收最久没被使用的图片资源。

资源(Resource)和原始数据 (Data)

Glide 自己实现了 DiskLruCache 作为文件缓存。Resource
和 Data 的使用同一个 DickCache,缓存在同一片区域。唯一的区别是 Key 不同。同一张原图可以产生很多 Resource。前者的 key 需要宽高和转换方式都相同,只匹配转换后的图片资源。后者只比较原图的 url 和标签,只匹配原图。

在查找缓存时,两者都返回 File 对象。Glide 会在回调方法中设置一个标志,用于区分是 Resource 还是 Data。如果是 Data,还要对查询到的缓存再进行一次转换,如果是 Resource ,就可以省略这一步。

缓存的 Key 如何生成

查找缓存时一共有三个地方会生成 Key:

  • 在查找内存中的活跃资源和内存缓存时,生成 EngineKey
  • 在文件缓存中查找转换过的图片时,生成 ResourceCacheKey
  • 在文件缓存中查找未经转换的原图时,生成 DataCacheKey

DataCacheKey 只需要匹配图片的来源和标签,保证是同一张源图片即可。而 EngineKey 和 ResourceCacheKey 除此之外还要匹配宽高,转换器,各种配置以及 ResourceClass。保证经转换后的资源也一模一样。

图片如何加载

图片资源的加载是交给 ModelLoader 来负责的。ModelLoader 会将具体的加载任务交给 DataFetcher 处理。Glide 会根据不同的 Model 使用不同的 DataFecther。比如我们最常见的加载图片 url,其实现类就是 HttpUrlFetcher,内部使用 HttpUrlConnection 来请求图片,最后输出一个 InputStream 获取图片数据。InputStream 经过 decoder 解码就能转换成我们需要的 Bitmap 或者 Drawable 了。

加载线程如何管理

Glide 中使用线程池管理加载线程,一共有四种:

1. SourceExecutor

默认使用这个线程池管理请求源图片的线程。它的核心线程数和最大线程数相等,队列采用优先级阻塞队列。也就是说,它只有固定个数的核心线程,且不会被回收。核心线程数的设置根据 Cpu 的核心数确定,且最多不超过4个。

2. DiskCacheExecutor

负责管理文件缓存的读写线程。单线程模型,只有一个核心线程,使用优先级阻塞队列。这种单线程模型可以避免文件的并发读写问题。

3. UnlimitedSourceExecutor

当请求配置中的 useUnlimitedSourceGeneratorPool 设置为 true 时,会使用这个线程池管理请求源图片的线程。这个线程池没有核心线程,最大线程数为 Integer.MAX_VALUE,这代表非核心线程的数量没有限制,每个线程的默认存活时间是10秒。另外,它采用 SynchronousQueue 队列,这个队列不存储任务,只要有新任务就会新建线程来处理。

4. AnimationExecutor

当请求配置中的 useAnimationPool 参数设置为 true 时,会使用 AnimationExecutor 管理请求源图片的线程。这个线程池没有核心线程,最大线程数取决于 Cpu 核心数,当核心数大于等于 4 时,最大线程数为2,否则为1。线程默认存活时间为10秒,使用优先级阻塞队列。

使用此线程可以避免在加载资源较大的图片时过多占用 Cpu,一般在加载 GIF 图片时使用。