Glide 调试、硬件位图、从 v3 迁移到 v4

Glide 调试

Glide 硬件位图

Glide 从 v3 迁移到 v4

大致目录:
//调试
* 本地日志 (Local Logs)
  * 请求错误
  * 预料之外的缓存丢失
  * 图片和本地日志丢失
    * 启动请求失败(Failing to start the request)
    * 未指定尺寸(Missing Size)
      * 自定义Target(Custom Targets)
      * Views
  * 请求监听器与定制日志

* Out of memory 错误
    * 过大的内存分配
    * 内存泄露

* 其他常见问题
  * “You can’t start or clear loads in RequestListener or Target callbacks”
//硬件位图
* 什么是硬件位图(Hardware Bitmaps)?

* 我们为什么应该使用硬件位图?

* 如何启用硬件位图?

* 如何禁用硬件位图?

* 哪些情况不能使用硬件位图?

* 使用硬件位图有什么缺点?
//从 v3 迁移到 v4
* 选项(Options)
  * RequestBuilder
  * 请求选项
  * 变换
  * 解码格式
  * 过渡选项
    * 交叉淡入 (Cross fade)
  * Generated API

* 类型 (Type) 与目标 (Target)
  * 选择资源类型
  * Drawables
  * Targets
    * 取消请求

* 配置
  * 应用程序
  * 程序库
  * 清单解析
  * using(), ModelLoader, StreamModelLoader.
    * ModelLoader
    * using()

一、调试

1.1 本地日志 (Local Logs)

如果你拥有设备的访问权限,你可以使用 adb logcat 或你的 IDE 查看一些日志。你可以使用 adb shell setprop log.tag.<tag_name> <VERBOSE|DEBUG> 操作为任何下面提到的标签 (tag) 开启日志。VERBOSE 级别的日志会显得更加冗余但包含更多有用的信息。根据你要查看的标签的不同,你可以把 VERBOSEDEBUG 级别的信息都尝试一下,以决定哪个级别的信息是你最需要的。

1.1.1 请求错误

最高级别和最容易理解的日志都通过 Glide 标签打印。Glide 标签将记录成功和失败的请求以及不同级别的详细信息,具体取决于日志级别。VERBOSE 会被用于记录成功的请求,DEBUG 则会打印出详细的错误信息。

你也可以通过手动调用 setLogLevel(int) 方法控制 Glide 标签的冗余度。setLogLevel 允许你在开发构建 (developer builds) 时启用更加冗余的日志,而在发布 (release builds) 构建时则关闭它们。

1.1.2 预料之外的缓存丢失

Engine 标签会详细记录请求被填充的全过程,并包括用于存储相应资源的完整内存缓存键。如果你正在尝试调试“内存中明明有这个图片,为什么没在另一个地方用到”的问题,那么 Engine 标签可以让你直观地比较两者的缓存键的区别。

对于每一个开始了的请求,Engine 标签将会记录这个请求将会从哪个地方加载完成:缓存,活动资源,已存在的加载过程,或者一个新的加载过程。

  • 缓存:意味着这个资源暂时没有被用到,但是在内存缓存中可用。

  • 活动资源:表示这个资源正在被另一个 Target 使用,一般是在一个 View 中。

  • 已存在的加载过程:表示这个资源虽然现在在内存中不可用,但是另一个 Target 已经在早先发起了对同一个资源的请求,并且这个请求还在处理中。

  • 新的加载过程:表示这个资源既不在内存中,也没有被其他地方请求过,那么这将触发一次新的加载。

1.1.3 图片和本地日志丢失

在某些情况下,你可能会发现某个图片永远不会加载出来,而且这个请求甚至还没有 Glide 标签和 Engine 标签的日志。这可能有以下一些原因。

  • 启动请求失败 (Failing to start the request)
    请检查你是否为你的请求调用了 into() 或者 submit() 方法。很显然,如果你忘记了调用这两个方法,Glide 不会认为你已经要求开始加载。

  • 未指定尺寸 (Missing Size)
    如果你确信你调用了 into()submit() 之一,并且仍然没有看到日志,那么最可能的解释是,Glide 无法决定你即将加载资源的 View 或 Target 的尺寸。

  • 自定义 Target (Custom Targets)
    如果你正在使用一个自定义的 Target ,请确保你实现了 getSize 方法并使用了非零的宽高来调用指定的回调方法,或者继承自一个已经为你实现了这个方法的 Target ,例如 ViewTarget

  • Views
    如果你只是在往一个 View 中加载资源,那么最大的可能是你的这个 view 要么还没有被布局 (layout) 过,要么被指定了零宽或高。View 的可见性被设置为 View.GONE 或它并没有被 attach,都会导致 view 不会被 layout 。如果 View 和/或它们的父控件被以特定方式组合 wrap_contentmatch_parent 来作为宽高,则 view 可能会收到一个无效的或为 0 的宽高值。你可以试验一下,将你的 view 设定为非 0 的尺寸,或在请求时使用 override(int, int) API 来为 Glide 传入一个特定的尺寸。

1.1.4 请求监听器与定制日志

如果你想使用编程的办法跟踪成功和失败信息、跟踪应用中的整体缓存命中率,或增加对本地日志的控制,你可以使用 RequestListener 接口。RequestListener 可以通过 RequestBuilder#listener() 方法来添加到单独的加载请求中。下面是一个使用示例:

Glide.with(fragment)
   .load(url)
   .listener(new RequestListener() {
       @Override
       boolean onLoadFailed(@Nullable GlideException e, Object model,
           Target<R> target, boolean isFirstResource) {
         // Log the GlideException here (locally or with a remote logging framework):
         Log.e(TAG, "Load failed", e);

         // You can also log the individual causes:
         for (Throwable t : e.getRootCauses()) {
           Log.e(TAG, "Caused by", t);
         }
         // Or, to log all root causes locally, you can use the built in helper method:
         e.logRootCauses(TAG);

         return false; // Allow calling onLoadFailed on the Target.
       }

       @Override
       boolean onResourceReady(R resource, Object model, Target<R> target,
           DataSource dataSource, boolean isFirstResource) {
         // Log successes here or use DataSource to keep track of cache hits and misses.

         return false; // Allow calling onResourceReady on the Target.
       }
    })
    .into(imageView);

请注意,每个 GlideException 都有多个 Throwable root cause。在 Glide 中可能有任意多的方法使得注册组件 (ModelLoader, ResourceDecoder, Encoder 等) 作用于从给定的模型 (URL, File 等)加载给定的资源 (Bitmap, GifDrawable 等)。每个 Throwable root cause 描述了一个特定的 Glide 组件组合为什么失败。理解某个特定请求为何失败可能需要检查所有的 root cause

然而,你也可能会发现某个单一的 root cause 比其他的要重要一些。例如你正在加载 URL 并试图找出特定的 HttpException (它意味着你的加载是由于一个网络错误而失败),你可以遍历所有的 root cause 并使用 instanceof 来检查其类型:

for (Throwable t : e.getRootCauses()) {
  if (t instanceof HttpException) {
    Log.e(TAG, "Request failed due to HttpException!", t);
    break;
  }
}

当然你也可以使用类似的迭代过程和 instanceof 操作符来检查 Http 错误之外其他你关心的异常类型。

为减少对象分配起见,你可以为多个加载重用相同的 RequestListener

1.2 Out of memory 错误

几乎所有的 OOM 错误都是因为宿主应用出了问题,而不是 Glide 本身。 应用里两种常见的 OOM 错误分别是:

  1. 过大的内存分配 (Excessively large allocations)

  2. 内存泄露 (Memory leaks),被分配的内存没有被释放

1.2.1 过大的内存分配

如果在打开一个单独页面或加载一个单独图片导致了 OOM,那么你的应用可能在加载一个不必要的大图。

使用 Bitmap 显示一张图片所需的内存数量为宽 (width) * 高 (height) * 每像素字节数 (bytes per pixel)。 每像素字节数取决于显示图片所使用的 Bitmap.Config,但通常对于 ARGB_8888 的位图来说,每个像素即为四个字节。因此,即使是一张普通的 1080P 图片也需要 8MB 内存。图片越大,所需要的内存就越多,因此一个 12M 像素的图片会要求相当庞大的 48MB 内存。

Glide 会将图片自动下采样 (downsample),这是基于 Target,ImageView 或 override() 提供的尺寸。如果你在 Glide 中看到了特别大的内存分配,通常意味着你的 Target 或 override() 提供的尺寸太大,或你使用了 Target.SIZE_ORIGINAL 而又恰好碰上了一个大图。

要解决这种过大的内存分配,请避免使用 Target.SIZE_ORIGINAL 并确保你的 ImageView 尺寸或你通过 override() 方法提供给 Glide 的尺寸是合理的。

1.2.2 内存泄露

如果在你的应用中持续重复特定步骤会逐步增加你应用的内存使用并最终导致 OOM ,你可能有内存泄露。

Android 官方文档中有很多关于追踪和调试内存使用的有用信息。为了调查内存泄露,你几乎肯定需要捕捉一个 heap dump 并查看 Fragments, Activities 和以及其他不再被使用但却仍被持有的对象。

要修复内存泄露,你需要对已销毁的 Fragment 或 Activity 在生命周期的合适时机移除对它们的引用,以避免持有过多的对象。使用 heap dump 来帮助查找你应用中持有其他内存的方式并在找到后移除不必要的引用。通常你可以从列出对 Bitmap 对象使用 (MAT 或其他内存分析器) 的最短路径(不含弱引用)开始,然后寻找可疑的引用链。你还可以在你的内存分析器中搜索 Activity 和 Fragment,以确保每个 Activity 不超过一个实例,并且 Fragment 的实例数目也在期望范围内。

1.3 其他常见问题

You can’t start or clear loads in RequestListener or Target callbacks

如果你尝试在一个 TargetRequestListener 里的 onResourceReadyonLoadFailed 中开始一次新的加载,Glide 将会抛出一个异常。之所以抛出这个异常,是因为要处理和回收这种在通知过程中的 (notifying) 加载对 Glide 来说是一个巨大的挑战。

好在这个问题很好解决。从 Glide 4.3.0 开始,你可以很轻松地使用 .error() 方法。这个方法接受一个任意的 RequestBuilder,它会且只会在主请求失败时开始一个新的请求:

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

对于 Glide 4.3.0 以前的版本,你也可以使用一个 Android Handlerpost 一个 Runnable 给你的请求:

private final Handler handler = new Handler();
...

Glide.with(fragment)
  .load(url)
  .listener(new RequestListener<Drawable>() {
      ...

      @Override
      public boolean onLoadFailed(@Nullable GlideException e, Object model, 
          Target<Drawable> target, boolean isFirstResource) {
        handler.post(new Runnable() {
            @Override
            public void run() {
              Glide.with(fragment)
                .load(fallbackUrl)
                .into(imageView);
            }
        });
      }
  )
  .into(imageView);

二、硬件位图

2.1 什么是硬件位图(Hardware Bitmaps)?

Bitmap.Config.HARDWARE 是一种 Android O 添加的新的位图格式。硬件位图仅在显存 (graphic memory) 里存储像素数据,并对图片仅在屏幕上绘制的场景做了优化。

2.2 我们为什么应该使用硬件位图?

因为硬件位图仅储存像素数据的一份副本。一般情况下,应用内存中有一份像素数据(即像素字节数组),而在显存中还有一份副本(在像素被上传到 GPU 之后)。而硬件位图仅持有 GPU 中的副本,因此:

  • 硬件位图仅需要一半于其他位图配置的内存;

  • 硬件位图可避免绘制时上传纹理导致的内存抖动。

2.3 如何启用硬件位图?

目前,你可以在 Glide 请求中将默认的 DecodeFormat 设置为 DecodeFormat.PREFER_ARGB_8888。要为应用中的所有请求都应用该操作,你需要在你的 GlideModule 中修改默认选项的 DecodeFormat

未来 Glide 将默认加载硬件位图而不需要额外的启用配置,只保留禁用的选项。

2.4 如何禁用硬件位图?

如果你需要禁用硬件位图,你应当仅在以下的一些缓慢的或根本不可用 (broken) 的情况下才尝试去做。你可以使用 disallowHardwareConfig() 来为一个特定的请求禁用硬件位图。

如果你在使用 generated API:

GlideApp.with(fragment)
  .load(url)
  .disallowHardwareConfig()
  .into(imageView);

或直接使用 RequestOptions

RequestOptions options = new RequestOptions().disallowHardwareConfig();
Glide.with(fragment)
  .load(url)
  .apply(options)
  .into(imageView);

2.5 哪些情况不能使用硬件位图?

在显存中存储像素数据意味着这些数据不容易访问到,在某些情况下可能会发生异常。已知的情形列举如下:

Canvas canvas = new Canvas(normalBitmap)
canvas.drawBitmap(hardwareBitmap, 0, 0, new Paint());
  • 在绘制位图的 View 上使用软件层 (software layer type)(例如,绘制阴影)
ImageView imageView = …
imageView.setImageBitmap(hardwareBitmap);
imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
  • 打开过多的文件描述符。每个硬件位图会消耗一个文件描述符。这里存在一个每个进程的文件描述符限制 ( Android O 及更早版本一般为 1024,在某些 O - MR1 和更高的构建上是 32K)。Glide 将尝试限制分配的硬件位图以保持在这个限制以内,但如果你已经分配了大量的文件描述符,这可能是一个问题。

  • 需要 ARGB_8888 Bitmaps 作为前置条件

  • 在代码中触发截屏操作,它会尝试使用 Canvas 来绘制视图层级。
    作为一个替代方案,在 Android O 以上版本你可以使用 PixelCopy

  • 共享元素过渡 (shared element transition)

以下是一个示例 trace:

java.lang.IllegalStateException: Software rendering doesn't support hardware bitmaps
  at android.graphics.BaseCanvas.throwIfHwBitmapInSwMode(BaseCanvas.java:532)
  at android.graphics.BaseCanvas.throwIfCannotDraw(BaseCanvas.java:62)
  at android.graphics.BaseCanvas.drawBitmap(BaseCanvas.java:120)
  at android.graphics.Canvas.drawBitmap(Canvas.java:1434)
  at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:529)
  at android.widget.ImageView.onDraw(ImageView.java:1367)
[snip]
  at android.view.View.draw(View.java:19089)
  at android.transition.TransitionUtils.createViewBitmap(TransitionUtils.java:168)
  at android.transition.TransitionUtils.copyViewImage(TransitionUtils.java:102)
  at android.transition.Visibility.onDisappear(Visibility.java:380)
  at android.transition.Visibility.createAnimator(Visibility.java:249)
  at android.transition.Transition.createAnimators(Transition.java:732)
  at android.transition.TransitionSet.createAnimators(TransitionSet.java:396)
[snip]

2.6 使用硬件位图有什么缺点?

在某些情况下为了避免打断用户,Bitmap 类将执行一次昂贵的显存复制。在某些使用这些方法的情况下,你应该根据使用这些缓慢方法的使用频率来考虑避免使用硬件位图配置。如果你确实要使用这些方法,系统将会打印一条信息: “Warning attempt to read pixels from hardware bitmap, which is very slow operation”,并触发一次 StrictMode#noteSlowCall

三、从 v3 迁移到 v4

3.1 选项 (Options)

Glide v4 中的一个比较大的改动是 Glide 库处理选项 (centerCrop(), placeholder() 等) 的方式。在 v3 版本中,选项由一系列复杂的异构建造者 (multityped builders) 单独处理。在新版本中,由一个单一类型的唯一一个建造者接管一系列选项对象。Glide 的 generated API 进一步简化了这个操作:它会合并传入建造者的选项对象和任何已包含的集成库里的选项,以生成一个流畅的 API。

3.1.1 RequestBuilder

对于这类方法:

listener()
thumbnail()
load()
into()

在 Glide v4 版本中,只存在一个 RequestBuilder 对应一个你正在试图加载的类型 (Bitmap, Drawable, GifDrawable 等)。 RequestBuilder 可以直接访问对这个加载过程有影响的选项,包括你想加载的数据模型(url, uri 等),可能存在的缩略图请求,以及任何的监听器。RequestBuilder 也是你使用 into() 或者 preload() 方法开始加载的地方:

RequestBuilder<Drawable> requestBuilder = Glide.with(fragment)
    .load(url);

requestBuilder
    .thumbnail(Glide.with(fragment)
        .load(thumbnailUrl))
    .listener(requestListener)
    .load(url)
    .into(imageView);

3.1.2 RequestOptions 请求选项

对于这类方法:

centerCrop()
placeholder()
error()
priority()
diskCacheStrategy()

大部分选项被移动到了一个单独的称为 RequestOptions 的对象中,

RequestOptions options = new RequestOptions()
    .centerCrop()
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .priority(Priority.HIGH);

RequestOptions 允许你一次指定一系列的选项,然后对多个加载重用它们:

RequestOptions myOptions = new RequestOptions()
    .fitCenter()
    .override(100, 100);

Glide.with(fragment)
    .load(url)
    .apply(myOptions)
    .into(drawableView);

Glide.with(fragment)
    .asBitmap()
    .apply(myOptions)
    .load(url)
    .into(bitmapView);

3.1.3 变换

Glide v4 里的 Transformations 现在会替换之前设置的任何变换。在 Glide v4 中,如果你想应用超过一个的 Transformation,你需要使用 transforms() 方法:

Glide.with(fragment)
  .load(url)
  .apply(new RequestOptions().transforms(new CenterCrop(), new RoundedCorners(20)))
  .into(target);

或使用 generated API:

GlideApp.with(fragment)
  .load(url)
  .transforms(new CenterCrop(), new RoundedCorners(20))
  .into(target);

3.1.4 解码格式

在 Glide v3,默认的 DecodeFormatDecodeFormat.PREFER_RGB_565,它将使用 Bitmap.Config.RGB_565,除非图片包含或可能包含透明像素。对于给定的图片尺寸,RGB_565 只使用 Bitmap.Config.ARGB_8888 一半的内存,但对于特定的图片有明显的画质问题,包括条纹 (banding) 和着色 (tinting)。为了避免 RGB_565 的画质问题,Glide 现在默认使用 ARGB_8888。结果是,图片质量变高了,但内存使用也增加了。

要将 Glide v4 默认的 DecodeFormat 改回 DecodeFormat.PREFER_RGB_565,请在 AppGlideModule 中应用一个RequestOption

@GlideModule
public final class YourAppGlideModule extends GlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
  }
}

3.1.5 过渡选项

对于这类方法:

crossFade()
animate()

控制从占位符到图片和/或缩略图到全图的交叉淡入和其他类型变换的选项,被移动到了 TransitionOptions 中。

要应用过渡(之前的动画),请使用下列选项中符合你请求的资源类型的一个:

  • GenericTransitionOptions

  • DrawableTransitionOptions

  • BitmapTransitionOptions

如果你想移除任何默认的过渡,可以使用 TransitionOptions.dontTransition()

过渡动画通过 RequestBuilder 应用到请求上:

Glide.with(fragment)
    .load(url)
    .transition(withCrossFade(R.anim.fade_in, 300));
交叉淡入 (Cross fade)

不同于 Glide v3,Glide v4 将不会默认应用交叉淡入或任何其他的过渡效果。每个请求必须手动应用过渡。

要为一个特定的加载应用一个交叉淡入变换效果,你可以使用:

import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;

Glide.with(fragment)
  .load(url)
  .transition(withCrossFade())
  .into(imageView);

或:

Glide.with(fragment)
  .load(url)
  .transition(
      new DrawableTransitionOptions
        .crossFade())
  .into(imageView);

3.1.6 Generated API

为了让使用 Glide v4 更简单轻松,Glide 现在也提供了一套可以为应用定制化生成的 API。应用可以通过包含一个标记了 AppGlideModule 的实现来访问生成的 API。

Generated API 添加了一个 GlideApp 类,该类提供了对 RequestBuilderRequestOptions 子类的访问。RequestOptions 的子类包含了所有 RequestOptions 中的方法,以及 GlideExtensions 中定义的方法。RequestBuilder 的子类则提供了生成的 RequestOptions 中所有方法的访问,而不需要你再手动调用 apply。举个例子:

在没有使用 Generated API 时,请求大概长这样:

Glide.with(fragment)
    .load(url)
    .apply(centerCropTransform()
        .placeholder(R.drawable.placeholder)
        .error(R.drawable.error)
        .priority(Priority.HIGH))
    .into(imageView);

使用 Generated API,RequestOptions 的调用可以被内联:

GlideApp.with(fragment)
    .load(url)
    .centerCrop()
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .priority(Priority.HIGH)
    .into(imageView);

你仍然可以使用生成的 RequestOptions 子类来应用相同的选项到多次加载中;但生成的 RequestBuilder 子类可能在多数情况下更为方便。

3.2 类型 (Type) 与目标 (Target)

3.2.1 选择资源类型

Glide 允许你指定你想加载的资源类型。如果你指定了一个超类型,Glide 会尝试加载任何可用的子类型。比如,如果你请求的是 Drawable,Glide 可能会加载一个 BitmapDrawable 或一个 GifDrawable。而如果你请求的是一个 GifDrawable,要么会加载出一个 GifDrawable,要么报错,只要图片不是 GIF 的话(即使它凑巧是一个完全有效的图片也是如此)。

默认请求的类型是 Drawable:

Glide.with(fragment).load(url)

如果要明确指定请求 Bitmap:

Glide.with(fragment).asBitmap()

如果要创建一个文件路径(本地图片的最佳选项):

Glide.with(fragment).asFile()

如果要下载一个远程文件到缓存然后创建文件路径:

Glide.with(fragment).downloadOnly()
// or if you have the url already:
Glide.with(fragment).download(url);

3.2.2 Drawables

Glide v3 版本中的 GlideDrawable 类已经被移除,支持标准的 Android DrawableGlideBitmapDrawable 也已经被删除,由 BitmapDrawable 代替之。

如果你想知道某个 Drawable 是否是动画 (animated),可以检查它是否为 Animatable 的实例。

boolean isAnimated = drawable instanceof Animatable;

3.2.3 Targets

onResourceReady 方法的签名做了一些修改。例如,对于 Drawables:

onResourceReady(GlideDrawable drawable, GlideAnimation<? super GlideDrawable> anim)

现在改为:

onResourceReady(Drawable drawable, Transition<? super Drawable> transition);

类似地,onLoadFailed 的签名也有一些变动:

onLoadFailed(Exception e, Drawable errorDrawable)

改为:

onLoadFailed(Drawable errorDrawable)

如果你想要获得更多导致加载失败的错误信息,你可以使用 RequestListener

取消请求

Glide.clear(Target) 方法被移动到了 RequestManager 中:

Glide.with(fragment).clear(target)

使用 RequestManager 清除之前由它启动的加载过程,通常能提高性能,虽然这并不是强制要求的。Glide v4 会为每一个 Activity 和 Fragment 跟踪请求,所以你需要在合适的层级去清除请求。

3.3 配置

在 Glide v3 中,配置使用一个或多个 GlideModule 来完成。而在 Glide v4 中,配置改为使用一个类似但稍微复杂的系统来完成。

3.3.1 应用程序

在早期版本中使用了一个 GlideModule 的应用,可以将它转换为一个 AppGlideModule。

在 Glide v3 中,你可能会有一个像这样的 GlideModule:

public class GiphyGlideModule implements GlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    builder.setMemoryCache(new LruResourceCache(10 * 1024 * 1024));
  }

  @Override
  public void registerComponents(Context context, Registry registry) {
    registry.append(Api.GifResult.class, InputStream.class, new GiphyModelLoader.Factory());
  }
}

在 Glide v4 中,你需要将其转换成一个 AppGlideModule ,它看起来像这样:

@GlideModule
public class GiphyGlideModule extends AppGlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    builder.setMemoryCache(new LruResourceCache(10 * 1024 * 1024));
  }

  @Override
  public void registerComponents(Context context, Registry registry) {
    registry.append(Api.GifResult.class, InputStream.class, new GiphyModelLoader.Factory());
  }
}

请注意,@GlideModule 注解不能省略。

如果你的应用拥有多个 GlideModule,你需要把其中一个转换成 AppGlideModule,剩下的转换成 LibraryGlideModule。除非存在 AppGlideModule,否则程序不会发现 LibraryGlideModule,因此你不能仅使用 LibraryGlideModule

3.3.2 程序库

拥有一个或多个 GlideModule 的程序库应该使用 LibraryGlideModule。程序库不应该使用 AppGlideModule ,因为它在一个应用里只能有一个。因此,如果你试图在程序库里使用它,将不仅会妨碍这个库的用户设置自己的选项,还会在多个程序库都这么做时造成冲突。

例如,v3 版本中 Volley 集成库的 GlideModule

public class VolleyGlideModule implements GlideModule {
  @Override
  public void applyOptions(Context context, GlideBuilder builder) {
    // Do nothing.
  }

  @Override
  public void registerComponents(Context context, Registry registry) {
    registry.replace(GlideUrl.class, InputStream.class, new VolleyUrlLoader.Factory(context));
  }
}

在 v4 版本中可以转换成为一个 LibraryGlideModule

@GlideModule
public class VolleyLibraryGlideModule extends LibraryGlideModule {
  @Override
  public void registerComponents(Context context, Registry registry) {
    registry.replace(GlideUrl.class, InputStream.class, new VolleyUrlLoader.Factory(context));
  }
}

3.3.3 清单解析

为了简化迁移过程,尽管清单解析和旧的 GlideModule 接口已被废弃,但它们在 v4 版本中仍被支持。AppGlideModule,LibraryGlideModule,与已废弃的 GlideModule 可以在一个应用中共存。

然而,为了避免检查元数据的性能天花板(以及相关的 bugs ),你可以在迁移完成后禁用掉清单解析,在你的 AppGlideModule 中复写一个方法:

@GlideModule
public class GiphyGlideModule extends AppGlideModule {
  @Override
  public boolean isManifestParsingEnabled() {
    return false;
  }

  ...
}

3.3.4 using(), ModelLoader, StreamModelLoader

3.3.4.1 ModelLoader

ModelLoader API 在 v4 版本中仍然存在,并且它的设计目标仍然和它在 v3 中一样,但有一些细节变化。

第一个细节,ModelLoader 的子类型如 StreamModelLoader,现在已没有存在的必要,用户可以直接实现 ModelLoader 。例如,一个 StreamModelLoader<File> 类现在可以通过 ModelLoader<File, InputStream> 的方式来实现和引用。

第二, ModelLoader 现在并不直接返回 DataFetcher,而是返回 LoadDataLoadData 是一个非常简单的封装,包含一个磁盘缓存键和一个 DataFetcher

第三, ModelLoaders 有一个 handles() 方法,这使你可以为同一个类型参数注册超过一个的 ModelLoader 。

将一个 ModelLoader 从 v3 API 转换到 v4 API,通常是很简单直接的。如果你在你的 v3 ModelLoader 中只是简单地返回一个 DataFetcher

public final class MyModelLoader implements StreamModelLoader<File> {

  @Override
  public DataFetcher<InputStream> getResourceFetcher(File model, int width, int height) {
    return new MyDataFetcher(model);
  }
}

那么你在 v4 替代类上需要做的仅仅只是封装一下这个 DataFetcher

public final class MyModelLoader implements ModelLoader<File, InputStream> {

  @Override
  public LoadData<InputStream> buildLoadData(File model, int width, int height,
      Options options) {
    return new LoadData<>(model, new MyDataFetcher(model));
  }

  @Override
  public void handles(File model) {
    return true;
  }
}

请注意,除了 DataFetcher 之外,模型也被传递给 LoadData 作为缓存键的一部分。这个规则为某些特殊场景提供了更多对磁盘缓存键的控制。大部分实现可以直接将 model 传入 LoadData,就像上面这样。

如果你仅仅是想为某些 model(而不是所有)使用你的 ModelLoader,你可以在你尝试加载 model 之前使用 handles() 方法来检查它。如果你从 handles 方法中返回了 false,那么你的 ModelLoader 将不能加载指定的 model ,即使你的 ModelLoader 类型 (在这个例子里是 File 和 InputStream) 与之匹配。

举个例子,如果你在某个指定文件夹下写入了加密的图片,你可以使用 handles 方法来实现一个 ModelLoader 以从那个特定的文件夹下解密图片,但是并不用于加载其他文件夹下的 File :

public final class MyModelLoader implements ModelLoader<File, InputStream> {
  private static final String ENCRYPTED_PATH = "/my/encrypted/folder";

  @Override
  public LoadData<InputStream> buildLoadData(File model, int width, int height,
      Options options) {
    return new LoadData<>(model, new MyDataFetcher(model));
  }

  @Override
  public void handles(File model) {
    return model.getAbsolutePath().startsWith(ENCRYPTED_PATH);
  }
}
3.3.4.2 using()

using API 在 Glide v4 中被删除了,这是为了鼓励用户使用 AppGlideModule 一次性地注册所有组件,避免对象重用。你无需每次加载图片时都创建一个新的 ModelLoader ;你应该在 AppGlideModule 中注册一次,然后交给 Glide 在每次加载时检查 model (即你传入 load() 方法的对象) 来决定什么时候使用你注册的 ModelLoader

为了确保你仅为特定的 model 使用你的 ModelLoader ,请像上面展示的那样实现 handles 方法:检查每个 model ,但仅在应当使用你的 ModelLoader 时才返回 true。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容