android 图片加载库(2)- Glide

说在开头

Glide 最早是 google 内部员工的作品,后来被 google 发掘力推,15,16年可火了,但是奈何渐渐不敌 Fresco ,Fresco 的占用内存低的优势真是太强大了,因为 Fresco 使用了系统的共享内存,而不是 JVM 虚拟机所属的内存块。但是 Glide 是非常适合用来学习的,Glide 的代码结构非常优秀,记得郭琳郭姐说过,要是能把 Glide 用心读一遍,都会获得脱胎换骨的变化的,对于我们加强,进化自己的 java 基本功大有裨益啊。

Glide 官方中文文档: Glide v4 快速高效的Android图片加载库 ,推荐大家都去看一下,尤其是打算研究源码的同学们


Glide 集成

Glide 现在处于 V 4.X 版本,较上一个版本 V 3.X 变动较大,大家注意一下。

Glide 依赖
repositories {
  mavenCentral()
  maven { url 'https://maven.google.com' }
}

dependencies {
    compile 'com.github.bumptech.glide:glide:4.5.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.5.0'
}

上面是官方给出的依赖地址,注意远程库地址可是 google 自己的地址,一般人都没写过这个地址,注意加上啊。第二个依赖是 Glide 的注解,版本号保持和 Glide 一致,这个注解的依赖可以不加,但是若是要全局设置 Glide 参数就必须使用了。

Glide 版本 android 编译版本
glide:4.5.0 appcompat-v7:27.0.2
glide:4.4.0 appcompat-v7:27.0.2
glide:4.3.1 appcompat-v7:26.1.0

注意 Glide 版本号对 android 编译版本有最低要求,不匹配就报错,这点要特殊注意。下面是 Glide 历史版本对 android 编译版本匹配要求:

Glide 版本 android 编译版本
glide:4.5.0 appcompat-v7:27.0.2
glide:4.4.0 appcompat-v7:27.0.2
glide:4.3.1 appcompat-v7:26.1.0

如果你需要使用不同的支持库版本,你需要在你的 build.gradle 文件里去从 Glide 的依赖中去除 "com.android.support"。例如,假如你想使用 v26 的支持库:

dependencies {
  implementation ("com.github.bumptech.glide:glide:4.5.0") {
    exclude group: "com.android.support"
  }
  implementation "com.android.support:support-fragment:26.1.0"
}

注意 Glide 依赖与支持库版本不兼容,会出现下面这个运行时异常:

java.lang.NoSuchMethodError: No static method getFont(Landroid/content/Context;ILandroid/util/TypedValue;ILandroid/widget/TextView;)Landroid/graphics/Typeface; in class Landroid/support/v4/content/res/ResourcesCompat; or its super classes (declaration of 'android.support.v4.content.res.ResourcesCompat' 
at android.support.v7.widget.TintTypedArray.getFont(TintTypedArray.java:119)

不推荐在依赖中使用 @aar ,如果必须这么做,请添加 transitive=true 以确保所有必要的类都被包含到你的 API 中:

dependencies {
    implementation ("com.github.bumptech.glide:glide:4.5.0@aar") {
        transitive = true
    }

不设置 transitive 可能会出现这个异常,另外若是还出现这个异常,就不要在依赖中使用 @aar

java.lang.NoClassDefFoundError: com.bumptech.glide.load.resource.gif.GifBitmapProvider
    at com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder.<init>(ByteBufferGifDecoder.java:68)
    at com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder.<init>(ByteBufferGifDecoder.java:54)
    at com.bumptech.glide.Glide.<init>(Glide.java:327)
    at com.bumptech.glide.GlideBuilder.build(GlideBuilder.java:445)
    at com.bumptech.glide.Glide.initializeGlide(Glide.java:257)
    at com.bumptech.glide.Glide.initializeGlide(Glide.java:212)
    at com.bumptech.glide.Glide.checkAndInitializeGlide(Glide.java:176)
    at com.bumptech.glide.Glide.get(Glide.java:160)
    at com.bumptech.glide.Glide.getRetriever(Glide.java:612)
    at com.bumptech.glide.Glide.with(Glide.java:684)
Glide 混淆
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
  **[] $VALUES;
  public *;
}

# for DexGuard only
-keepresourcexmlelements manifest/application/meta-data@value=GlideModule

Glide 所需权限

Glide 权限这块就是需要 : 读写 SD 卡权限,其他没有了

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Generated API - Glide 的全局设置

这个是 Glide V4 版本新添加的内容,全局设置的 option 可以在单个请求中替换。Glide 给大家提供这样一个全局设置的位置,就是想让我们的可以服用这些关于图片加载的公共业务设置,以方便我们可以更高度,更优秀的组织我们的图片加载代码,甚至是编写我们自己的图片加载业务库,大家不要忘了,时间是不断流逝的,说不准哪天就是我们替换图片加载库的时候了呢,所以优良的代码架构设计和业务功能框架的封装就显得有味必要了。这里 Glide 新版本给我们能提供更为便利的操作。

2个全局设置基类 AppGlideModule / LibraryGlideModule

首先我们需要按照如下这样,在我们的 app 中声明一个类

@GlideModule
public final class MyAppGlideModule extends AppGlideModule {
    ......
}

这个类需要继承 AppGlideModule 这个 Glide 提供给我们的类,注意关于这个类有以下要求:

  • Application 级别的 module 也就是我们常用的 app module中,我们要继承 AppGlideModule 这个类
  • 而 Library 级别的 module 我们就要继承 LibraryGlideModule 这个类
  • 注意上面2个类不要用混,要不无法正常使用代码的
  • 必须使用 @GlideModule 这个注解来修饰我们的这个类,@GlideModule 这个注解是 Glide 用来遍历代码,查询我们定义的全局配置类的关键
  • 我们定义的全局配置类可以是空的实现,虽然这个要求不是必须的,但是官方还是办强制的要求我们这么做
  • 从上面的2个不同的基类来看,其实我们可以舍弃 AppGlideModule 而使用 LibraryGlideModule 的,我们会把 app 根据业务和功能拆分成业务和基础功能 module 的,在所有的 module 中,图片加载的配置应该是统一的,这样我们就要把图片加载也做成一个单独的功能 module ,或是抽象到业务基础功能库 module 中,这样在 Library 级别的 module 中,我们就只想使用 LibraryGlideModule 这个父类了。

然后我们就可以使用 Glide 的统一入口来调用 Glide 的 API 了。上面说过来了 Glide 的设计思路是很棒的,使用 bulider 建造者模式来统一存储,设置相关的各种 option 。使用全局静态单例的方式来给我们提供统一的公共入口,这样的 API 非常之简洁,是我们编写基础功能库的最优秀方案。

Glide 的公共入口有2个类,按照不同的配置情况来使用:

  • Glide :我们若是不配置全局配置类的话,就用 Glide 这个类
  • GlideApp :我们若是配置了全局配置类的话,就用 GlideApp 这个类

这2个类的区别除了全局配置以外,就是 API 的使用上了,Glide 在 API 使用上是链式调用的写法,很方便。使用 GlideApp 这个类,对我们使用 API 没影响,但是 Glide 这个类,我们只能链式使用最基础的2个 API:

GlideApp.with(context)
             .load("图片地址")
             .into(getImage());

所用从使用上来看,官方是半强制的让我们使用 GlideApp 这个类,也就是让我们进行全局配置。

全局配置类中我们可以做什么

还是得从 2个全局设置基类 AppGlideModule / LibraryGlideModule 开始,毕竟这是全局配置的入口,先来看看2个类的定义:

public abstract class AppGlideModule extends LibraryGlideModule implements AppliesOptions {
  
  public boolean isManifestParsingEnabled() {
    return true;
  }

  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    // Default empty impl.
  }
}
public abstract class LibraryGlideModule implements RegistersComponents {

  @Override
  public void registerComponents(Context context, Glide glide, Registry registry) {
    // Default empty impl.
  }
}

可以看到 AppGlideModule 也是扩展自 LibraryGlideModule 的,LibraryGlideModule 只有一个注册组件的方法,AppGlideModule 则实现了 AppliesOptions 接口,在 applyOptions 这个方法里可以进行我们对 Glide 的全局配置

看到这里各位看官是不是有疑问了,上文说到我们要把图片加载放到单独的基础功能 module 中,那么在 module 里就不能 AppGlideModule ,而只能使用 LibraryGlideModule ,但是 LibraryGlideModule 不能支持我们配置全局参数啊,这不就是矛盾了嘛,我做了下测试:

  • 基本设计:
    写3个 module :app / BitmapActivity / BitmapLoader 。 app 是主工程;BitmapActivity 提供一个页面,用来测试加载图片;BitmapLoader 提供公共图片加载 API ,在这 module 中声明一个 AppGlideModule 的子类出来。
  • 测试流程:
    主功能有个按钮,点击一下跳转到 BitmapActivity 提供的页面去加载图片,然后点击一个按钮加载图片
  • 测试结果:
    正常没问题,可以加载出图片来
  • 结论猜测:
    可能官方文档的中文描述有点歧义,Glide 使用的注解在编译时扫描指定注解的方式查找 AppGlideModule 的子类,Glide 可以做到扫描 aar 包,那么这个 AppGlideModule 的子类我们写在哪个 module 就不重要了,只要 AppGlideModule 的子类唯一,注解可以扫描的到就行了,LibraryGlideModule 的意义应该是在 module 中优先级高于全局的 AppGlideModule 。上面这是我的猜测。
好了啰嗦了好半天,我们看看都有哪些可以设置的

配置入口在 AppGlideModule 的 applyOptions 方法,配置选项如下:

  • MemoryCache :内存缓存策略
  • BitmapPool : Bitmap 池缓存策略
  • DiskCache :磁盘缓存策略
  • DefaultRequestOptions :默认请求选项
  • 未捕获异常策略
  • LogLevel :日志级别

Glide 缓存使用LRU 算法,内存缓存使用的是 LruResourceCache ,磁盘缓存使用的是 DiskLruCacheWrapper ,位图池使用的是 LruBitmapPool 。各级缓存都可以指定大小,也可以替换成自己的实现,但是官方不建议这么做,可能在资源回收这里造成位置错误。需要说的是默认磁盘大小为 250 MB,具体的还是推荐大家去看官方文档:配置 | Glide最新版V4使用指南

默认请求选项这个是比较有用的,这里我们可以设置很多东西,具体有啥后面说,下面的官方文档的例子

@GlideModule
public class YourAppGlideModule extends AppGlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    builder.setDefaultRequestOptions(
        new RequestOptions()
          .format(DecodeFormat.RGB_565) // 解码格式
          .disallowHardwareBitmaps());
  }
}

要说的是 V 3.X 版本默认解码格式是 RGB_565,对于有透明通道的图片来说可能会造成色差问题,从 V 4.X 版本开始,默认解码格式改成了 ARGB_8888

日志这块没什么东西,就是调整一下日志的级别罢了

@GlideModule
public class YourAppGlideModule extends AppGlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    builder.setLogLevel(Log.DEBUG);
  }
}

基本 api

GlideApp.with(activity)
.load(url) // 图片地址
.override(200,200) // 设置图片尺寸
.priority(Priority.HIGH) // 加载优先级
.into(imageView); // 目标 view

Glide的基本使用还是很简单的,上面几个方法都很简单,没有复杂的参数,也是一看就都懂的

占位符

这个简单,一看都懂,不多说

  • placeholder
    加载完成之前显示的
  • error
    请求失败时显示的
  • fallback
    url 为null 时显示的,但是优先级低于 error,此时有设置了 error 就优先显示 error,没有时才显示 fallback

注意有一种写法,在请求失败时也就是 error 时,可以再请求一次 error 的图片,注意下面的写法:

Glide.with(fragment)
  .load(primaryUrl)
  .error(Glide.with(fragment)
      .load(fallbackUrl))
  .into(imageView);

这是 error 中需要传入的是一个 RequestBuilder<T> 对象,T 的乐星跟着上面走,我们平时写的 Glide.with(fragment).load(primaryUrl).into(imageView) 这个链式调用本质就是生成一个 RequestBuilder<T> 对象,所以可以作为参数传进去。很有意思把,这样的写法,我也是第一次见,后面还有这样的应用。

Thumbnail 缩略图

有一些优秀的后台设计是可以让我们再请求大图之前给我们提供一张专属的缩略图,而不是大陆货色,这时 Glide 的 Thumbnail 缩略图就发挥作用了。Glide 的 Thumbnail 缩略图和大图其实是2个平发的请求,一般缩略图比大图返回的要快,所有先显示缩略图,但大图的优先级比缩略图高,要是大图先回来,那么就不会显示缩略图了。缩略图我们可以配置单独的一个 url 地址,也可以用大图的地址,指定缩略图的尺寸或是缩放比例。

 GlideApp.with(activity)
                .load(url)
                // 请求独立的缩略图
                .thumbnail(GlideApp.with(activity)
                        .load(thumbnailUrl))
                // 请求原图的缩略图,指定大小,长宽不同
                .thumbnail(GlideApp.with(activity)
                        .load(url).override(200, 300))
                // 请求原图的缩略图,指定大小,长宽相同
                .thumbnail(GlideApp.with(activity)
                        .load(url).override( 300 ))
                // 请求原图的缩略图,指定大小,指定缩放比例
                .thumbnail(0.25f)

下面的 API 就要讲究一个先来后到的顺序问题了,下面说2个东西,Transformation 变换,transition 过度。图片加载本质也是一个网络请求,一个请求出去后,过一回就会回来,这时 Glide 的 API 会先跑 Transformation 变换的设置,因为这时我们拿到原始资源了,我们可以做任何我们想干的事,裁剪,转换类型等。然后我们按照我们秀娥想法处理完原始资源过后就要传递给 view 去显示了,这时 Glide 的 API 会跑 transition 过度的设置了,过度其实就是动画,目的是为了让 view 的图像切换显得不是很突兀,要贴近自然,一般都是做一个渐变动画。

那么接下来我们按照顺序,先说 Transformation 变换的 API ,再来说 transition 过度的 API

Transformation 变换

Transformation 变换的目的是对原始资源进行定制化处理,我们在这里可以干一下几种事情:

  • 类型变换,默认返回的是 Drawable 类型的对象
  • 对图片按照 imageview scaleType 进行裁剪
  • 裁剪呈圆形,圆角或其他任何形状,这是最常见的需求
  • 自定义变换
  • 没有变换
  • 多个 Transformation 效果叠加,Transformation 默认只能用一个,想同时用多个,得打个集合包进去

类型变换 API

GlideApp.with(activity)
                .asBitmap() 
                .asDrawable()
                .asFile()
                .asGif()

不用说了吧,很直白的,你懂的,默认的是 asDrawable()

scaleType 裁剪

  GlideApp.with(activity)
                .load(url)
                .centerCrop()
                .fitCenter()
                .circleCrop()
                .dontTransform

具体效果我说一下,centerCrop 和 fitCenter 都是他们原本的效果,什么变化,circleCrop 是裁剪成圆形,这里我找了下没找到圆角的裁剪,这里系统自带了圆形裁剪还是很方便的,其他圆形裁剪后面会介绍。最后一个是没有变化

自定义变换需要继承 BitmapTransformation 这个类,具体的去看管饭发个文档吧,不重复写了: Glide - 变换

多个 Transformation 效果叠加

 GlideApp.with(activity)
                .load(url)
                .transforms(new FitCenter(), new CircleCrop())

transforms 里面传入的是可变参数,可以传入多个,说实话,代码写到现在,真的感觉这个可惨参数真是秒啊,要不就要多写一个集合对象,代码上就不能像现在这么简洁,流畅,舒心了。

transition 过度

承接上文,我们获取完原始数据,然后按照我们定制的处理过后,就该是我们去如何显示的步奏了,目前都是采用一个渐变动画做过度的,这个 transition 的意思就是这样的一个动画了。

过渡动画执行时机:

  • 磁盘缓存
  • 本地资源
  • 远程资源
  • 内存资源时不会触发过度动画

过渡动画效果:

  • 淡入
  • 交叉淡入
  • 不过渡
  • V4.X 版本默认是没有动画效果的
// 淡入
.transition( DrawableTransitionOptions.withCrossFade() )
// 交叉淡入
.transition(DrawableTransitionOptions.withCrossFade(new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true)))
或者
DrawableTransitionOptions options = new DrawableTransitionOptions().crossFade(new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true));
.transition(options)
// 没有动画
TransitionOptions.dontTransition()
或者
.dontAnimate()

根据图片资源类型的不同,TransitionOptions 有3种:

  • BitmapTransitionOptions
  • DrawableTransitionOptions
  • GenericTransitionOptions 通用型

动画也可以自定义:
1.实现TransitionFactory
2.重写build()
可以控制图片在内存缓存上是否执行动画。

具体写法参考DrawableCrossFadeFactory,然后调用

TransitionOptions的with(TransitionFactory transitionFactory)加载。

有一点要说啊,淡入的效果是给加载出来的图片一个渐变进入的动画,然后遮挡占位图。若是使用圆形变换裁剪图片,而使用全尺寸占位图的话,那么图片边缘就会把占位图露出来,所以占位图的形状一定要和图片裁剪成的形状相同徐熬过才好。


添加监听器

GlideApp.with(context)
                .load(url)
                .placeholder(R.mipmap.ic_launcher)
                .error(R.mipmap.ic_launcher)
                .centerCrop()
                .listener(new RequestListener<Drawable>() {
                    @Override
                    public boolean onLoadFailed(@Nullable GlideException e, Object o, Target<Drawable> target, boolean b) {
                        Log.i(TAG, "图片加载失败 ");
                        return false;
                    }

                    @Override
                    public boolean onResourceReady(Drawable drawable, Object o, Target<Drawable> target, DataSource dataSource, boolean b) {
                        Log.i(TAG, "图片加载完成: ");
                        return false;
                    }
                })
                .into(imageView);

API 是很简单的, 但是我也没用过啊,也许是让我们实现图片加载进度显示的。


缓存策略

Glide 有6种缓存策略:

  • DiskCacheStrategy.AUTOMATIC
    自动缓存,默认的缓存策略,效果 = SOURCE + RESULT
  • DiskCacheStrategy.NONE
    跳过磁盘缓存
  • DiskCacheStrategy.SOURCE
    仅仅只缓存原来的全分辨率的图像,改变尺寸或是变换过后的都不缓存
  • DiskCacheStrategy.RESULT
    仅仅缓存最终的图像,即,降低分辨率后的(或者是转换后的)
  • DiskCacheStrategy.ALL
    缓存所有版本的图像(默认行为)
  • skipMemoryCache(true)
    跳过内存缓存, 加载的图片不存入内存中,也不从内存中查询缓存

对于我们经常要进行变换的图片来说,变换过后的图片可能对你有很打的影响,那么忽略内存缓存是很有必要的,以此类推,这几种缓存策略还是不难理解的,大部分情况下默认缓存模式就够我们用了。但是要注意默认缓存模式中,磁盘缓存和内存缓存是一样的,内存缓存了什么都会同步到磁盘缓存中,比如变换的图片也会进行磁盘缓存。


统一参数配置

还记得 AppGlideModule 这个类吧,我们可以在这个类的子类中进行统一配置,可以配置的属性如下:

  • Placeholders 占位符
  • Transformations 变换
  • Caching Strategies 缓存策略
  • 组件特定参数:编码质量,解码参数等。
@GlideModule
public class MyLibraryGlideModule extends AppGlideModule {

    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        super.applyOptions(context, builder);

        RequestOptions requestOptions = new RequestOptions();
        requestOptions.placeholder(R.drawable.ic_launcher_xx)
                // 占位符
                .error(R.drawable.ic_launcher_xx)
                .format(DecodeFormat.PREFER_ARGB_8888)
                // transform 变换,裁剪等
                .circleCrop();

        // 过度动画参数
        DrawableTransitionOptions transitionOptions =            DrawableTransitionOptions.withCrossFade(new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true));

        builder.setDefaultRequestOptions(requestOptions);
        builder.setDefaultTransitionOptions(Drawable.class, transitionOptions);
    }
}

资源清理

Glide 的资源清理要小心,所有的在 Glide 相关 API 中获取的 Drawable,Bitmap 都是由 Glide 统一负责回收,重用,所以我们不能私自进行 recycle 等回收操作,有可能会引起错误。

但是我们还是可以进行资源回收操作的,有3个操作:

  • clean
    清除正在显示的 view 和图片资源的关联,注意这时 view 会返回显示占位图或是空白图片
GlideApp.with(activity).clear(image);
  • clearMemory
    清除内存缓存,官方不建议我们这么做,但是自行判断下在用哟见大量图片的页面关闭时,要不要手动回收下内存资源,我觉得这样还是有必要的。Glide 在页面关闭时做的也只是停止这个页面当前的所有请求。这个方法是同步方法,在主线程运行
GlideApp.get(activity).clearMemory();
  • clearDiskCache
    清楚磁盘缓存,这个我觉得意义不大, 默认的磁盘缓存是 250M 大小,而且磁盘会缓存图片的原始数据,变换,裁剪过后的数据都会缓存,这样其实是很方便我们的,一般情况下我们是不会触发这个选项的,除了有一些 app 在设置里可以清楚缓存。这个方法注意是需要异步运行的
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... voids) {
                GlideApp.get(activity).clearDiskCache();
                return null;
            }
        };
        task.execute();
    }

参考资料: