原文链接:点击查看

资源

Glide 中的资源包含很多东西,例如 Bitmapbyte[] 数组, int[] 数组,以及大量的 POJO 。无论什么时候,Glide 都会尝试重用这些资源,以限制你应用中的内存抖动数量。

好处

任何尺寸的对象的过多分配都会显著增加你应用中的垃圾回收 (GC)。虽然 Android 较新的 ART 运行时的 GC 惩罚比 Dalvik 运行时要低,但无论你使用什么设备,过多内存分配都会降低应用的性能。

Dalvik

Dalvik 设备 (Lollipop 之前)在过多分配时将不得不面对特别大的代价,值得在这里讨论一下。

Dalvik 有两种基本的 GC 模式, GC_CONCURRENT 和 GC_FOR_ALLOC ,这两种你都可以在 logcat 中看到。

  • GC_CONCURRENT 对于每次收集将阻塞主线程大约 5ms 。因为每个操作都比一帧(16ms)要小,GC_CONCURRENT 通常不会造成你的应用丢帧。
  • GC_FOR_ALLOC 是一种 stop-the-world 收集,可能会阻塞主线程达到 125ms 以上。GC_FOR_ALLOC 几乎每次都会造成你的应用丢失多个帧,导致视觉卡顿,特别是在滑动的时候。

不幸的是,Dalvik 似乎甚至连适度的分配(例如一个 16kb 的缓冲区)都处理得不是很好。重复的中等分配,或即使单次大的分配(比如说一个 Bitmap ),将会导致 GC_FOR_ALLOC 。因此,你分配的内存越多,就会招来越多 stop-the-world 的 GC,而你的应用将有更多的丢帧。

通过复用中到大尺寸的资源, Glide 可以帮你尽可能地减少这种 GC,以保持应用的流畅。

Glide 如何追踪和重用资源

Glide 采用较为宽容的办法来处理资源重用。Glide 会在它相信某个资源可以安全地复用时才这么做,但它并不要求调用者在每次请求之后都回收资源。除非某个调用者显式地表示它已经用完了某个资源(见下文),资源将不会被回收或重用。

引用计数

为决定某个资源是否正在被使用,以及什么时候可以安全地被重用,Glide 为每个资源保持了一个引用计数。

增加引用计数

每次调用 into() 来加载一个资源,这个资源的引用计数会被加一。如果相同的资源被加载到两个不同的 Target,则在两个加载都完成后,它的引用计数将会为二。

减少引用计数

引用计数仅在调用者通过以下方式表示它们用完资源后会减少:

  1. 在加载资源的 ViewTarget 上调用 clear()
  2. 在这个ViewTarget 上调用对另一个资源请求的 into 方法。

释放资源

当引用计数到达 0 时,这个资源会被释放并被返回给 Glide 以重用。当资源被返回给 Glide 以重用以后,继续使用它是不安全的,因此以下行为是 不安全的

  1. 使用 getImageDrawable 来取回 ImageView 中加载的 BitmapDrawable,并使用某种方式展示它( setImageDrawable,动画,或 TransitionDrawable 或其他任何方式 )。
  2. 使用 SimpleTarget 来将一个资源加载到 View,但没有实现 onLoadCleared() 方法并在其中将资源从 View 中移除。
  3. 对 Glide 加载的任何 Bitmap 调用 recycle()

在清理对应的 ViewTarget 之后还保持对资源的引用是不安全的,因为这个资源可能已经被销毁,或被重用于展示一个不同的图片,这可能导致未定义行为,图形损坏,或甚至导致继续使用该资源的应用崩溃。例如,在被释放回 Glide 之后, Bitmap 可能会被存储在一个 BitmapPool 中,并在未来的某个时刻被用重用于保存一张新图片的字节数据,或者它们已经被调用了 [recycle()]。在这两种情况下继续引用这个 Bitmap 并期待它们保持原始图像都是不安全的。

池化 (Pooling)

尽管 Glide 的大部分回收逻辑主要针对 Bitmap,但所有的 Resource 实现均可实现 recycle() 方法并将它们包含的任意可重用的数据池化。 ResourceDecoder 可以返回开发者希望的任意 [Resource] API,因此开发者可以定制或提供额外的池化规则,只需要实现它们自己的 ResourceResourceDecoder

特别地,对于 Bitmap,Glide 提供了一个 BitmapPool 接口,以允许 Resource 获取和重用 [Bitmap] 对象。 Glide 的 [BitmapPool] 可以从任意的 Context 中使用 Glide 的单例获取到:

Glide.get(context).getBitmapPool();

类似地,希望为 Bitmap 池化施加更多控制的用户可以直接实现他们自己的 BitmapPool,然后可以通过 GlideModule 的方式提供给 Glide。参见配置页.

常见错误

然而,允许池化让保证用户不会误用资源或Bitmap变得很困难。 Glide 会在可能的地方尝试添加一些断言,但是因为我们并不持有底层的 Bitmap,我们无法保证调用者在告诉我们 clear() 或一个新请求之后,会立即停用这些资源。

资源重用错误的征兆

有多种迹象可能暗示 Bitmap 或其他在 Glide 中被池化的资源出了问题。我们列出了一些最常见的现象,但这不是一个完备的列表。

Cannot draw a recycled Bitmap

Glide 的 BitmapPool 是固定大小的。当 Bitmap 从中被踢出而没有被重用时,Glide 将会调用 recycle()。如果应用在向 Glide 指出可以安全地回收之后 “不经意间” 继续持有 Bitmap,则应用可能尝试绘制这个 Bitmap,进而在 onDraw 方法中造成崩溃。

一种可能的情况是,一个目标被用于两个ImageView,而其中一个在 Bitmap 被放到 BitmapPool 中后仍然试图访问被回收后的 Bitmap。基于以下因素,要复现这种复用错误可能很困难:1)Bitmap 何时被放入池中,2)Bitmap 何时被回收,3)何种尺寸的 BitmapPool 和内存缓存会导致 Bitmap 的回收。可以在你的 GlideModule 中加入下面的代码片段,以使这个问题更容易复现:

@Override
public void applyOptions(Context context, GlideBuilder builder) {
    int bitmapPoolSizeBytes = 1024 * 1024 * 0; // 0mb
    int memoryCacheSizeBytes = 1024 * 1024 * 0; // 0mb
    builder.setMemoryCache(new LruResourceCache(memoryCacheSizeBytes));
    builder.setBitmapPool(new LruBitmapPool(bitmapPoolSizeBytes));
}

上面的代码确保没有内存缓存,且 BitmapPool 的尺寸为0;因此 Bitmap 如果恰好没有被使用,它将立刻被回收。这是为了调试目的让它更快出现。

Can’t call reconfigure() on a recycled bitmap

资源将在它们不再被使用时被返回到 Glide 的 BitmapPool 中。这里的内部实现基于 Request(它控制着Resource) 的生命周期管理。如果在这些 Bitmap 上调用了 recycle(),但它们仍然在池中,就会使 Glide 无法重用它们而导致你的应用崩溃并抛出这个信息。这里的一个关键点是,这个崩溃很可能发生在未来的某个点,而不在这个违例代码的执行处!

View 在图片之间闪烁或相同的图像在多个 View 中展示

如果一个 Bitmap 被多次返回到 BitmapPool 中,或它已被返回到池中单仍然被一个 View 持有,另一个图片可能会被解码到这个 Bitmap 对象中。如果这种情况发生,就会使得 Bitmap 的内容会被替换为新的图片。 在这个过程中,View 可能仍然试图绘制这个 Bitmap,而这将导致原始的 View 展示一张新的图片。

重用错误的原因

一些常见的重用错误原因已被列在下面。就像上面的征兆一样,要列出全面的列表是很困难的,但是在尝试调试应用程序中的重用错误时,这些是您应该考虑的一些事情。

尝试往相同的 Target 加载两个不同的资源

在 Glide 中没有安全的办法来加载多个资源到单一的 Target 中。用户可以使用 thumbnail() API 来加载一系列资源到一个 Target,但也仅仅在下一个 onResourceReady() 调用之前才可以安全地引用早前的一个资源。

通常一个更好的答案是使用第二个 View 并将第二章图片加载到这第二个 View 上。 ViewSwitcher 可以很好地允许你在两个单独请求的不同图片之间做交叉淡入效果 (cross fade)。你可以仅添加一个 ViewSwitcher 在你的布局中,使用两个 ImageView 作为其子控件,然后使用两次 into(ImageView)方法,每次一个子控件,来加载两张图片。

对于绝对要求将多个资源加载到相同 View 的用户,可以使用两个单独的 Target。为确保每个加载都不会取消另一个,用户还需要避免使用 ViewTarget 子类,或使用一个自定义的 [ViewTarget] 子类并复写(override)其 setRequest()getRequest() 以使得它们不使用 View 的 tag 来存储 Request。这属于高级用法,一般不推荐。

往Target中加载资源,清除或重用Target,并继续引用该资源

最简单的比较这个错误的办法是确保所有对资源的引用都在 onLoadCleared() 调用时置空。通常,加载一个 Bitmap 然后对 Target 解引用,并且不要再次调用 into()clear(),这样是安全的。然而,加载了一个 Bitmap,清除这个 Target,并在之后继续持有 Bitmap 引用是不安全的。类似地,加载资源到一个 View 上然后从 View 中获取这个资源 (通过 getImageDrawable() 或任何其他手段),并在其他某个地方继续引用它,也是不安全的。

Transformation<Bitmap> 中回收原始Bitmap

正如在 变换 的 JavaDoc 中所说,传入 transform() 的原始 Bitmap 将会自动被回收,只要这个 Transformation 返回的 Bitmap 和原始传入 transoform() 的不是同一个实例。这是和其他加载库很重要的一个不同,例如 Picasso。 BitmapTransformation 提供了 Glide 的资源创建的模板,但它的回收是在内部完成的,所以不管是 Transformation 还是 BitmapTransformation 都不要回收传入的 BitmapResource

另外值得注意的是,任何定制的 BitmapTransformationBitmapPool 中创建、但没有从 transform() 返回的中间 Bitmap,都会被返回到 BitmapPool 或被回收,但不会两种情况同时发生。你永远都不应该 recycle() 从 Glide 中创建的 Bitmap