【开源库剖析】Glide 4.8.0 源码解析

876 阅读7分钟

最近因为对项目的图片库做了功能拓展和优化,花了点时间研究了下Glide,输出了总共6篇解析文章:
图片框架 - Glide 4.11.0源码走读
图片框架 - Glide自定义配置和组件及Registry机制
图片框架 - Glide加载webp动图流程解析
图片框架 - Glide解码webp动图浅析
图片框架 - Glide缓存机制解析
图片框架 - Glide磁盘缓存研究

本篇文章对整个Glide源码宏观剖析做一个简单总结。因为项目是基于Glide4.8.0,所以方便起见,分析的源码也是4.8.0版本。为了阅读方便,文章就尽量不贴对应代码了,Glide代码量确实有点多,想了解详情的可以参考上面的6篇文章,里面有详细源码解析。

一、Glide整体结构

1.1整个Glide主干功能:

image.png

1.2 图片加载整体执行流程:

image.png

Glide作为外部调用的入口函数,主要收集请求参数,构建一个图片请求,交由engine去获取图片资源,engine先从内存获取,如果活跃资源中有直接拿,如果没有则尝试去lruCache中获取,如果也没有则通过EngineJob线程池,发起一个异步任务即:DecodeJob来进行磁盘和网络获取。这里磁盘缓存策略会根据DiskCacheStrategy来设置,它主要是配置对图片原始数据流缓存以及解码转码后资源缓存两种类型数据。Generator是具体的图片资源获取管理类,它经由ModelLoader-LoadData-具体Fetcher的层层内部类调用关系最终交由对应的Fetcher类去处理图片资源获取的任务。成功获取资源后,会层层回调回来到DecodeJob来做解码工作(如果是原始资源),解码通过ModelLoader-LoadPath-具体Decoder的层层内部类调用关系最终交由对应的Decoder去做图片资源解码任务。后续就是将图片设置到目标控件上去。 这里,DecodeJob本身是通过Stage来调度自身不同任务类型,另外,网络IO和本地IO是分属于不同的DecodeJob,也就是两者任务的转换是需要通过EngineJob切线程来完成的。最后,Registry支持用户自定义配置和组件。

1.3 图片加载中数据转换流程

image.png

二、Glide核心类图

image.png

这里简单例举了部分核心类之间的关系:

  • Glide: 入口类。 
  • RequestBuilder: 收集参数,构建Request和Target交由RequestManager去统一处理。
  • RequestManager:左膀右臂:TargetTracker负责Target对应页面生命周期绑定、RequestTracker负责发起Request请求。
  • Engine:加载引擎。
  • EngineJob:线程池。
  • DcodeJob:异步任务,负责图片获取和解码。
  • Generator:负责获取图片资源,这里分了三类(ResourceCache对应解码后缓存、DataCache对应原始资源缓存、Source对应网络请求),通过ModelLoader最终匹配到对应的Fetcher来执行具体获取图片资源任务。
  • Target:显示图片的目标控件。

三、相关执行流程

3.1 首先看Glide.with(this).load(url).into(imageView) 整体调用流程

3.1.1 with

image.png

with主要干两件事:

  • 图片加载绑定对应页面生命周期; 生命周期分为application 和 非application两种。分别通过ApplicationLifecycle、ActivityFragmentLifecycle来管理生命周期。

  • 初始化RequestManager;

3.1.2 load

image.png

load主要干一件事情:

  • 通过RequestManager初始化RequestBuilder。收集model和requestOption相关请求参数,为后续into封装request做准备。
3.1.3 into

image.png

into主要干了三件事:

  • 封装并发起request。
  • request获取图片数据。
  • 将图片显示到View上。

3.2 Glide整体缓存机制

3.2.1内存加载数据逻辑

image.png

3.2.2 内存、磁盘、网络请求整体存取逻辑

image.png

这里简单总结下:

取逻辑: 内存 > 磁盘 > 网络请求

  • 内存: 上次刚被加载的资源(activeResources) > (最近被加载的资源)lruCache。

  • 磁盘: 如果有主动设置DiskCacheStrategy,则按设置来。如果配置的是DiskCacheStrategy.ALL:则是取转换之后的资源(ResourceCache) > (DataCache)原始资源

  • 网络请求: 走网络请求获取图片资源流。

存逻辑:

  • 内存: 当前被加载的图片资源存到activeResources中,下次加载资源切换时,当前activeResources会remove然后被转移到lruCache。

  • 磁盘: 网络请求成功之后,获取到资源流,然后看DiskCacheStrategy是否支持磁盘缓存,如果支持,会通过回调在SourceGenerator中通过cacheData进行资源缓存。

3.3 Glide磁盘缓存流程

网络请求成功后onDataReady回调触发的磁盘缓存流程: 这里有两个关键节点:原始数据流缓存和解码后资源缓存。SourceGenerator本身除了发起网络请求之外,也会在网络请求成功后,在DiskCacheStrategy允许的条件下对原始数据进行磁盘缓存,其次在DecodeJob数据解码成功后,在DiskCacheStrategy允许的条件下对解码后的资源进行磁盘缓存。

这里有个问题:如果网络请求成功后缓存的图片原始数据流本身有问题,解码失败的话,框架顶多是不会缓存解码后资源,但是对有问题的原始数据缓存不会做处理,这就会导致后续获取图片时按优先级优先获取有问题的图片原始数据缓存,导致问题。

解决分析: 客户端层面:首先客户端无法单独判断是否是解码失败,因为Glide网络请求失败、解码失败、IO失败等等都统一抛的是onLoadFailed,其次对外暴露的api也没有单独清理一张图片的,只有批量清理磁盘缓存,可以批量清,但这样会影响到整体性能。也可以通过加signature绕过去,但是这样每次都会去请求网络,相当于不走缓存,最后也可以调整DiskCacheStrategy配置,不做原始数据缓存了,这个倒是可以。

解决:我本身也不太喜欢直接gradle引三方库,这样一来不好拓展功能、二来不好定位修复问题。如果开源,我一般都会引入源码,所以这里我直接改了源码:

DecodeJob.java

private void decodeFromRetrievedData() {
  if (Log.isLoggable(TAG, Log.VERBOSE)) {
    logWithTimeAndKey("Retrieved data", startFetchTime,
       "data: " + currentData
           + ", cache key: " + currentSourceKey
           + ", fetcher: " + currentFetcher);
  }
  Resource<R> resource = null;
  try {
    resource = decodeFromData(currentFetcher, currentData, currentDataSource);
  } catch (GlideException e) {
    e.setLoggingDetails(currentAttemptingKey, currentDataSource);
   throwables.add(e);
  }
  if (resource != null) {
    notifyEncodeAndRelease(resource, currentDataSource);
  } else {
    //解码失败,删除之前缓存磁盘的原始数据文件
    diskCacheProvider.getDiskCache().delete(new DataCacheKey(currentSourceKey, signature));
    runGenerators();
  }
}

具体分析过程参考文章:图片框架 - Glide磁盘缓存研究

好了,经过上面的图文并茂的解析,应该对Glide不管是整体还是部分都有了一个比较直观的了解了。

四、Glide中编译时注解+APT的应用

最后再简单介绍下Glide中编译时注解+APT的应用。

注解的玩法主要有两个场景:运行期和编译期。

  • 运行期:主要是注解+反射,注解提供标签,反射对注解类来做逻辑。
  • 编译期:注解+APT+反射,这里APT(Annotation Processor Tool)注解处理器,编译期会动态生成类,而类的内容要么自己手动拼串组装类内容,要么使用JavaPoet封装好的工具来组装类内容。

而Glide中,单例调用get()初始化时:

private static GeneratedAppGlideModule getAnnotationGeneratedGlideModules(Context context) {
  GeneratedAppGlideModule result = null;
  try {
    Class<GeneratedAppGlideModule> clazz =
        (Class<GeneratedAppGlideModule>)
            Class.forName("com.bumptech.glide.GeneratedAppGlideModuleImpl");
   result =
        clazz.getDeclaredConstructor(Context.class).newInstance(context.getApplicationContext());
  } catch (ClassNotFoundException e) {
  ... 
 }
  return result;
}

这里的com.bumptech.glide.GeneratedAppGlideModuleImpl就是APT动态生成的:

image.png

源码对应的Processor路径:

image.png

这里就不详细分析GeneratedAppGlideModuleImpl的生成了,它最终的功能是针对manifest 和 注解两种注册方式分别调用其applyOptions和registerComponents来触发自定义配置和组件。

Demo地址:github.com/Zhto0/Glide…

好了就写这么多吧,Glide总体来说还是比较复杂的,本篇文章主要是对Glide做一个宏观分析,以及工作中牵涉到的部分功能进行了浅析。在这个宏观了解的基础上,应该能够对Glide框架局部问题的定位和分析提供一点帮助。当然文章中如有不对地方,欢迎批评指正!