一、目标
1.1 关于 Target
在 Glide 中,Target
是介于请求和请求者之间的中介者的角色。Target 负责展示占位符,加载资源,并为每个请求决定合适的尺寸。被使用得最频繁的是 ImageViewTargets
,它用于在 ImageView 上展示占位符、Drawable 和 Bitmap 。用户还可以实现自己的 Target ,或者从任何可用的基类派生子类。
1.2 指定目标
into(Target)
方法不仅仅用于启动每个请求,它同时也指定了接收请求结果的 Target:
Target<Drawable> target =
Glide.with(fragment)
.load(url)
.into(new Target<Drawable>() {
...
});
Glide 提供了一个辅助方法 into(ImageView)
,它接受一个 ImageView 参数并为其请求的资源类型包装了一个合适的 ImageViewTarget
:
Target<Drawable> target =
Glide.with(fragment)
.load(url)
.into(imageView);
1.3 取消和重用
你可能注意到 into(Target)
和 into(ImageView)
都返回了一个 Target 实例。如果你重用这个 Target 在将来开始一个新的加载,则之前开始的任何请求都会被取消,它们使用的资源将被释放:
Target<Drawable> target =
Glide.with(fragment)
.load(url)
.into(new Target<Drawable>() {
...
});
...
// Some time in the future:
Glide.with(fragment)
.load(newUrl)
.into(target);
你也可以使用返回的 Target 来 clear()
之前的加载,这将在不需要开始新的加载的情况下释放掉任何相关资源:
Target<Drawable> target =
Glide.with(fragment)
.load(url)
.into(new Target<Drawable>() {
...
});
...
// Some time in the future:
Glide.with(fragment).clear(target);
Glide 的 ViewTarget
子类使用了 Android Framework 的 getTag()
和 setTag()
方法来存储每个请求的相关信息,因此如果你在使用 ViewTarget
或在往 ImageView
中加载图片,你可以直接重用或清理这个 View
:
Glide.with(fragment)
.load(url)
.into(imageView);
// Some time in the future:
Glide.with(fragment).clear(imageView);
// Or:
Glide.with(fragment)
.load(newUrl)
.into(imageView);
此外,仅对 ViewTarget
而言,你可以在每次加载或清理调用时都传入一个新的实例,而 Glide 仍然可以从 View 的 tag 中取回之前一次加载的信息:
Glide.with(fragment)
.load(url)
.into(new DrawableImageViewTarget(imageView));
// Some time in the future:
Glide.with(fragment)
.load(newUrl)
.into(new DrawableImageViewTarget(imageView));
注意,除非你的 Target 继承自
ViewTarget
,或实现了setRequest()
和getRequest()
并允许你从新的 Target 实例中取回上一次加载的信息,否则这种使用方法将不奏效。
清理
当你完成了对资源(Bitmap
,Drawable
等)的使用时,及时清理(clear
)你创建的这些 Target
是一个好的实践。即使你认为你的请求已经完成了,也应该使用 clear()
以使 Glide 可以重用被这次加载使用的任何资源 (特别是 Bitmap
)。未调用 clear()
会浪费 CPU 和内存,阻塞更重要的加载,甚至如果你在同一个 surface
(View
,Notification
,RPC
等) 上有两个 Target
,可能会引发图片显示错误。对于像 SimpleTarget
这种无法从一个新实例里跟踪前一个请求的 Target
来说,及时清理尤为重要。
1.4 尺寸 (Sizes and dimensions)
默认情况下,Glide 使用目标通过 getSize()
方法提供的尺寸来作为请求的目标尺寸。这允许 Glide 选取合适的 URL,下采样 (downsample)
,裁剪和变换合适的图片以减少内存占用,并确保加载尽可能快地完成。
1.4.1 View 目标
ViewTarget
通过检查 View 的属性和/或使用一个 OnPreDrawListener
在 View 绘制之前直接测量尺寸来实现 getSize()
方法。因此,Glide 可以自动调整大部分图片以匹配目标 View。加载更小的图片可使 Glide 更快地完成加载 (在缓存到磁盘以后),并使用更少的内存,在图片尺寸一致时还可以增加 Glide 的 BitmapPool
的命中率。
ViewTarget 使用以下逻辑:
如果 View 的布局参数尺寸 > 0 且 > padding,则使用该布局参数;
如果 View 尺寸 > 0 且 > padding,使用该实际尺寸;
如果 View 布局参数为
wrap_content
且至少已发生一次layout
,则打印一行警告日志,建议使用Target.SIZE_ORIGINAL
或通过override()
指定其他固定尺寸,并使用屏幕尺寸为该请求尺寸;其他情况下(布局参数为
match_parent
, 0, 或wrap_content
且没有发生过layout
),则等待布局完成,然后回溯到步骤 1。
有时在使用 RecyclerView
时,View 可能被重用且保持了前一个位置的尺寸,但在当前位置会发生改变。为了处理这种场景,你可以创建一个新的 ViewTarget
并为 waitForLayout()
方法传入 true
:
@Override
public void onBindViewHolder(VH holder, int position) {
Glide.with(fragment)
.load(urls.get(position))
.into(new DrawableImageViewTarget(holder.imageView, /*waitForLayout=*/ true));
强大的尺寸管理
通常 Glide 在显式地为加载的 View 设置了 dp
尺寸时提供了最快且最可预测的结果。如果无法达到这一点,Glide 也通过 onPreDrawListener
提供了为 layout_weight
,match_parent
和其他相对尺寸的完备鲁棒的支持。最后,如果这些都无法达成,Glide 应该也为 wrap_content
提供了合理的行为。
后备方案
在任何情况下,如果 Glide 看起来获取了错误的 View 尺寸,你都可以手动覆盖来纠正它。你可以选择扩展 ViewTarget
实现你自己的逻辑,或者使用 RequestOption
里的 override()
方法。
1.4.2 定制目标
如果你正在使用一个 Target
且你将要加载的不是可以允许你派生 ViewTarget
的 View, 你讲需要实现 getSize()
方法。
实现 getSize()
可能最简单的方案是直接调用回调:
@Override
public void getSize(SizeReadyCallback cb) {
cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
}
使用 Target.SIZE_ORIGINAL
可能非常低效,或如果你的图片足够大可能引发 OOM 。作为替代方案,你也可以为你的 Target 的构造器传入一个尺寸,并把这些尺寸提供给回调:
public class CustomTarget<T> implements Target<T> {
private final int width;
private final int height;
public CustomTarget(int width, int height) {
this.width = width;
this.height = height;
}
...
@Override
public void getSize(SizeReadyCallback cb) {
cb.onSizeReady(width, height);
}
}
如果你的应用内使用一致的图片尺寸,或你确切地知道你需要的尺寸,你也可以传入一个特定尺寸的集合。如果你不知道所需的具体尺寸,但可以异步地得出结果,你也可以使用列表持有在 getSize()
中给出你的任何回调,然后执行你的异步过程并稍后在你得出尺寸之后通知你持有的这些回调。
如果你持有了这些回调,请确保同时实现 removeCallback
以避免内存泄露。
如果需要一个示例,请参考 ViewTarget 中的逻辑。
1.5 动画资源和定制目标
如果你只是要加载 GifDrawable
,或任何其他资源类型到一个 View,你应该总是尽可能地使用 into(ImageView)
。除了优雅的处理或新发起请求之外,Glide 的大部分 ViewTarget
实现已经为你处理了 Drawable 动画。如果你确实必须使用定制的 ViewTarget
,请确保继承自 ViewTarget
或在新请求开始之前和展示资源结束之后严格地清理从 into(Target)
返回的 Target。
如果你并非往 View 中加载图片,而直接使用 ViewTarget
或使用了定制的 Target 比如 SimpleTarget
且你正在加载一个动画资源例如 GifDrawable
,你需要确保在 onResourceReady()
中调用 start()
来启动这个动画:
Glide.with(fragment)
.asGif()
.load(url)
.into(new SimpleTarget<>() {
@Override
public void onResourceReady(GifDrawable resource, Transition<GifDrawable> transition) {
resource.start();
// Set the resource wherever you need to use it.
}
});
如果你加载的是 Bitmap
或 GifDrawable
,你可以判断这个可绘制对象是否实现了 Animatable
:
Glide.with(fragment)
.load(url)
.into(new SimpleTarget<>() {
@Override
public void onResourceReady(Drawable resource, Transition<GifDrawable> transition) {
if (resource instanceof Animatable) {
resource.start();
}
// Set the resource wherever you need to use it.
}
});
二、过渡
2.1 关于过渡
在 Glide 中,Transitions
(直译为”过渡”) 允许你定义 Glide 如何从占位符到新加载的图片,或从缩略图到全尺寸图像过渡。Transition 在单一请求的上下文中工作,而不会跨多个请求。因此,Transitions 并不能让你定义从一个请求到另一个请求的动画(比如,交叉淡入效果)。
2.2 默认过渡
不同于 Glide v3,Glide v4 将不会默认应用交叉淡入或任何其他的过渡效果。每个请求必须手动应用过渡。
2.3 标准行为
Glide 提供了很多的过渡效果,用户可以手动地应用于每个请求。Glide 的内置过渡以一致的方式运行,并且将根据加载图像的位置在某些情况下避免运行。
在 Glide 中,图像可能从四个地方中的任何一个位置加载出来:
Glide 的内存缓存
Glide 的磁盘缓存
设备本地可用的一个源文件或 Uri
仅远程可用的一个源 Url 或 Uri
如果图像从 Glide 的内存缓存中加载出来,Glide 的内置过渡将不会执行。然而,在另外三种场景下,Glide 的内置过渡都会被执行。
2.4 指定过渡
TransitionOptions
用于给一个特定的请求指定过渡。 每个请求可以使用 RequestBuilder
中的 transition()
方法来设定 TransitionOptions
。还可以通过使用 BitmapTransitionOptions
或 DrawableTransitionOptions
来指定类型特定的过渡动画。对于 Bitmap 和 Drawable 之外的资源类型,可以使用 GenericTransitionOptions
。
2.5 性能提示
Android 中的动画代价是比较大的,尤其是同时开始大量动画的时候。 交叉淡入和其他涉及 alpha (透明度)
变化的动画显得尤其昂贵。 此外,动画通常比图片解码本身还要耗时。在列表和网格中滥用动画可能会让图像的加载显得缓慢而卡顿。为了提升性能,请在使用 Glide 向 ListView
, GridView
, 或 RecyclerView
加载图片时考虑避免使用动画,尤其是大多数情况下,你希望图片被尽快缓存和加载的时候。作为替代方案,请考虑预加载,这样当用户滑动到具体的 item
的时候,图片已经在内存中了。
2.6 常见错误
2.6.1 对占位符和透明图片交叉淡入
Glide 的默认交叉淡入 (cross fade)
效果使用了 TransitionDrawable
。它提供两种动画模式,由 setCrossFadeEnabled()
控制。当交叉淡入被禁用时,正在过渡的图片会在原先显示的图像上面淡入。当交叉淡入被启用时,原先显示的图片会从不透明过渡到透明,而正在过渡的图片则会从透明变为不透明。
在 Glide 中,默认禁用了交叉淡入,这样通常看起来要好看一些。实际的交叉淡入,如上所述对两个图片同时改变 alpha (透明度)
值,通常会在过渡的中间造成一个短暂的白色闪屏,这个时候两个图片都是部分不透明的。
不幸的是,虽然禁用交叉淡入通常是一个比较好的默认行为,当待加载的图片包含透明像素时仍然可能造成问题。当占位符比实际加载的图片要大,或者图片部分为透明时,禁用交叉淡入会导致动画完成后占位符在图片后面仍然可见。如果你在加载透明图片时使用了占位符,你可以启用交叉淡入,具体办法是调整 DrawableCrossFadeFactory
里的参数并将结果传到 transition()
中。
2.6.1 在多个请求间交叉淡入
Transitions
并不能让你在不同请求中加载的两个图像之间做过渡。当新的加载被应用到 View 或 Target 上时,Glide 默认会取消任何已经存在的请求。因此,如果你想加载两个不同的图片并在它们之间做动画,你无法直接通过 Glide 来完成。等待第一个加载完成并在 View 外持有这个 Bitmap 或 Drawable ,然后开始新的加载并手动在这两者之间做动画,诸如此类的策略看起来有效,但是实际上不安全,并可能导致程序崩溃或图像错误。
相反,最简单的办法是使用包含两个 ImageView 的 ViewSwitcher
来完成。将第一张图片加载到 getNextView()
的返回值里面,然后将第二张图片加载到 getNextView()
的下一个返回值中,并使用一个 RequestListener
在第二张图片加载完成时调用 showNext()
。为了更好地控制,你也可以使用 Android 开发者文档指出的策略。但要记住与 ViewSwitcher
一样,仅在第二次图像加载完成后才开始交叉淡入淡出。
2.7 定制过渡
如果要定义一个自定义的过渡动画,你需要完成以下两个步骤:
实现
TransitionFactory
。使用
DrawableTransitionOptions#with
来将你自定义的TransitionFactory
应用到加载中。
如果要改变你的 transition
的默认行为,以更好地控制它在不同的加载源(内存缓存,磁盘缓存,或 uri
)下是否被应用,你可以检查一下你的 TransitionFactory
中传递给 build()
方法的那个 DataSource
。
如需示例代码,请查看 DrawableCrossFadeFactory。
三、资源重用
3.1 资源
Glide 中的资源包含很多东西,例如 Bitmap
,byte[]
数组,int[]
数组,以及大量的 POJO
。无论什么时候,Glide 都会尝试重用这些资源,以限制你应用中的内存抖动数量。
3.2 好处
任何尺寸的对象的过多分配都会显著增加你应用中的垃圾回收 (GC)。虽然 Android 较新的 ART 运行时的 GC 惩罚比 Dalvik 运行时要低,但无论你使用什么设备,过多内存分配都会降低应用的性能。
3.3 Dalvik
Dalvik 设备 (Lollipop 之前) 在过多分配时将不得不面对特别大的代价,值得在这里讨论一下。
Dalvik 有两种基本的 GC 模式,GC_CONCURRENT
和 GC_FOR_ALLOC
,这两种你都可以在 logcat 中看到。
GC_CONCURRENT
:当我们的应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发 GC 操作来释放内存。对于每次收集将阻塞主线程大约 5ms 。因为每个操作都比一帧 (16 ms) 要小,GC_CONCURRENT
通常不会造成你的应用丢帧。GC_FOR_ALLOC
:表示是在堆上分配对象时内存不足触发的 GC。是一种stop-the-world
收集,可能会阻塞主线程达到 125ms 以上。GC_FOR_ALLOC
几乎每次都会造成你的应用丢失多个帧,导致视觉卡顿,特别是在滑动的时候。
不幸的是,Dalvik 似乎甚至连适度的分配(例如一个 16 kb 的缓冲区)都处理得不是很好。重复的中等分配,或即使单次大的分配(比如说一个 Bitmap ),将会导致 GC_FOR_ALLOC
。因此,你分配的内存越多,就会招来越多 stop-the-world
的 GC,而你的应用将有更多的丢帧。
通过复用中到大尺寸的资源, Glide 可以帮你尽可能地减少这种 GC,以保持应用的流畅。
3.4 Glide 如何追踪和重用资源
Glide 采用较为宽容的办法来处理资源重用。Glide 会在它相信某个资源可以安全地复用时才这么做,但它并不要求调用者在每次请求之后都回收资源。除非某个调用者显式地表示它已经用完了某个资源,资源将不会被回收或重用。
3.4.1 引用计数
为决定某个资源是否正在被使用,以及什么时候可以安全地被重用,Glide 为每个资源保持了一个引用计数。
3.4.1.1 增加引用计数
每次调用 into()
来加载一个资源,这个资源的引用计数会被加一。如果相同的资源被加载到两个不同的 Target
,则在两个加载都完成后,它的引用计数将会为二。
3.4.1.2 减少引用计数
引用计数仅在调用者通过以下方式表示它们用完资源后会减少:
在加载资源的 View 或 Target 上调用
clear()
。在这个 View 或 Target 上调用对另一个资源请求的
into()
方法。
3.4.1.3 释放资源
当引用计数到达 0 时,这个资源会被释放并被返回给 Glide 以重用。当资源被返回给 Glide 以重用以后,继续使用它是不安全的,因此以下行为是不安全的:
使用
getImageDrawable
来取回ImageView
中加载的Bitmap
或Drawable
,并使用某种方式展示它 (setImageDrawable
,动画,或TransitionDrawable
或其他任何方式 )。使用
SimpleTarget
来将一个资源加载到 View,但没有实现onLoadCleared()
方法并在其中将资源从 View 中移除。对 Glide 加载的任何 Bitmap 调用
recycle()
。
在清理对应的 View 或 Target 之后还保持对资源的引用是不安全的,因为这个资源可能已经被销毁,或被重用于展示一个不同的图片,这可能导致未定义行为,图形损坏,或甚至导致继续使用该资源的应用崩溃。例如,在被释放回 Glide 之后, Bitmap 可能会被存储在一个 BitmapPool
中,并在未来的某个时刻被用重用于保存一张新图片的字节数据,或者它们已经被调用了 recycle()
。在这两种情况下继续引用这个 Bitmap 并期待它们保持原始图像都是不安全的。
3.5 池化 (Pooling)
尽管 Glide 的大部分回收逻辑主要针对 Bitmap,但所有的 Resource 实现均可实现 recycle()
方法并将它们包含的任意可重用的数据池化。ResourceDecoder
可以返回开发者希望的任意 Resource API,因此开发者可以定制或提供额外的池化规则,只需要实现它们自己的 Resource
和 ResourceDecoder
。
特别地,对于 Bitmap,Glide 提供了一个 BitmapPool
接口,以允许 Resource 获取和重用 Bitmap 对象。Glide 的 BitmapPool
可以从任意的 Context 中使用 Glide 的单例获取到:
Glide.get(context).getBitmapPool();
类似地,希望为 Bitmap 池化施加更多控制的用户可以直接实现他们自己的 BitmapPool
,然后可以通过 GlideModule
的方式提供给 Glide。参见配置页。
3.6 常见错误
然而,允许池化让保证用户不会误用资源或 Bitmap 变得很困难。 Glide 会在可能的地方尝试添加一些断言,但是因为我们并不持有底层的 Bitmap,我们无法保证调用者在告诉我们 clear()
或一个新请求之后,会立即停用这些资源。
3.6.1 资源重用错误的征兆
有多种迹象可能暗示 Bitmap 或其他在 Glide 中被池化的资源出了问题。下面列出了一些最常见的现象。
3.6.1.1 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
如果恰好没有被使用,它将立刻被回收。这是为了调试目的让它更快出现。
3.6.1.2 Can’t call reconfigure() on a recycled bitmap
资源将在它们不再被使用时被返回到 Glide 的 BitmapPool
中。这里的内部实现基于 Request
(它控制着 Resource
) 的生命周期管理。如果在这些 Bitmap 上调用了 recycle()
,但它们仍然在池中,就会使 Glide 无法重用它们而导致你的应用崩溃并抛出这个信息。这里的一个关键点是,这个崩溃很可能发生在未来的某个点,而不在这个违例代码的执行处!
3.6.1.3 View 在图片之间闪烁或相同的图像在多个 View 中展示
如果一个 Bitmap 被多次返回到 BitmapPool
中,或它已被返回到池中但仍然被一个 View 持有,另一个图片可能会被解码到这个 Bitmap 对象中。如果这种情况发生,就会使得 Bitmap
的内容会被替换为新的图片。 在这个过程中,View 可能仍然试图绘制这个 Bitmap
,而这将导致原始的 View 展示一张新的图片。
3.6.2 重用错误的原因
一些常见的重用错误原因已被列在下面。
3.6.2.1 尝试往相同的 Target 加载两个不同的资源
在 Glide 中没有安全的办法来加载多个资源到单一的 Target 中。用户可以使用 thumbnail()
API 来加载一系列资源到一个 Target,但也仅仅在下一个 onResourceReady()
调用之前才可以安全地引用早前的一个资源。
通常一个更好的答案是使用第二个 View 并将第二章图片加载到这第二个 View 上。 ViewSwitcher
可以很好地允许你在两个单独请求的不同图片之间做交叉淡入效果 (cross fade)
。你可以仅添加一个 ViewSwitcher
在你的布局中,使用两个 ImageView
作为其子控件,然后使用两次 into(ImageView)
方法,每次一个子控件,来加载两张图片。
对于绝对要求将多个资源加载到相同 View 的用户,可以使用两个单独的 Target。为确保每个加载都不会取消另一个,用户还需要避免使用 ViewTarget
子类,或使用一个自定义的 ViewTarget
子类并复写其 setRequest()
和 getRequest()
以使得它们不使用 View 的 tag 来存储 Request
。这属于高级用法,一般不推荐。
3.6.2.2 往 Target 中加载资源,清除或重用`Target,并继续引用该资源
最简单的避免这个错误的办法是确保所有对资源的引用都在 onLoadCleared()
调用时置空。通常,加载一个 Bitmap 然后对 Target 解引用,并且不要再次调用 into()
或 clear()
,这样是安全的。然而,加载了一个 Bitmap,清除这个 Target,并在之后继续持有 Bitmap 引用是不安全的。类似地,加载资源到一个 View 上然后从 View 中获取这个资源 (通过 getImageDrawable()
或任何其他手段),并在其他某个地方继续引用它,也是不安全的。
3.6.2.3 在 Transformation<Bitmap> 中回收原始 Bitmap
正如在变换的 JavaDoc 中所说,传入 transform()
的原始 Bitmap 将会自动被回收,只要这个 Transformation
返回的 Bitmap 和原始传入 transoform()
的不是同一个实例。这是和其他加载库很重要的一个不同,例如 Picasso。 BitmapTransformation
提供了 Glide 的资源创建的模板,但它的回收是在内部完成的,所以不管是 Transformation
还是 BitmapTransformation
都不要回收传入的 Bitmap 或 Resource。
另外值得注意的是,任何定制的 BitmapTransformation
从 BitmapPool
中创建、但没有从 transform()
返回的中间 Bitmap,都会被返回到 BitmapPool
或被回收,但不会两种情况同时发生。你永远都不应该 recycle()
从 Glide 中创建的 Bitmap。