1、概述
Glide可以说是目前最常用的Android图片加载框架,类似的框架还有Picasso、Fresco和UniversalImageLoader等。另外volley也是有图片加载的功能的,只是volley太久没用,都忘记了。这些框架里,最推荐使用的就是Glide,因为它使用的人最多、依旧在不断地更新维护且不需要使用框架定制的View。
那么Glide有哪些能力呢?且看:
- 占位符:在加载目标图片成功之前,先展示一个默认的图片而不是一片空白,提升用户体验
- 图片变换:在加载图片成功后,可以先对图片进行某些变换,然后才展示在View中。可以使用内置的变换,也可以自定义变换
- 自定义目标:Glide可以把图片加载到任意的目标中,而不只是ImageView,甚至不只是View,只要实现Target接口即可
- 过渡动画:加载成功后,通过动画完成展示的过程
- 缓存:缓存能力当然必不可少了,而且Glide的缓存机制相当强大
- 加载GIF:加载GIF图片是很多框架所不支持的
- 自定义配置:可以通过定义个中module来实现各种自定义的配置
- RecyclerView防错位:RecyclerView的Item是会回收的,而我们加载图片往往是异步操作。这就导致如果我们手动地异步加载图片,可能会出现Item已经被回收且重新利用,这时候再把加载好的图片展示在开始加载前传入的ImageView,就会出现图片的错位。而Glide自动处理了这个问题。
看了这么多激动人心的功能接下来我们一起看看Glide的使用和它的实现原理吧。由于Glide功能强大,代码也比较多,本文会相对简略,请见谅。
2、基本使用
Glide的使用非常简单,最常用的用法只需要一句:
Glide.with(this).load(uri).into(imageView)
其中with有好几个重载方法,可以接收View、Context、Activity、Fragment等类型参数;load也有很多重载方法,可以接收Uri、File、String、byte[]、Bitmap等等类型参数;into方法同样有重载方法,可以接收ImageView、Target等类型参数。这就使得我们可以在大部分场景下都可以如此简单地一行代码完成图片的加载。
Glide较为复杂的使用如下:
Glide.with(this).load(uri)
.placeholder(R.mipmap.ic_launcher) // 占位符
.error(R.mipmap.ic_launcher) // 加载失败时展示的图片
.addListener(object : RequestListener<Drawable> { // 添加监听
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
// do sth
return false
}
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
// do sth
return false
}
})
.transition(transition) // 设置过渡
.transform(transform) // 设置变换
.override(100, 200) // 自定义尺寸
.skipMemoryCache(true) // 跳过内存缓存,如果不跳过,不设置此项即可
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) // 设置磁盘缓存策略
.into(imageView)
Glide还有很多设置的选项,这里只列出一些常用的。其中有很多功能都很简单,比如占位符、加载失败时默认的图片、监听器等等,接下来会着重讲述图片变换、过渡动画、缓存、自定义配置以及RecyclerView防错位。
3、图片变换和Bitmap重用
图片变换可以这样写
Glide.with(this)
.load("https://gank.io/images/25d3e3db2c1248bb917c09dc4f50a46f")
.transform(object : Transformation<Bitmap> {
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
// 暂时忽略
}
override fun transform(
context: Context,
resource: Resource<Bitmap>,
outWidth: Int,
outHeight: Int
): Resource<Bitmap> {
val newResource = ...
return newResource
}
})
.into(iv)
其中transform方法接收一个Transformation<Bitmap>对象,通过这个对象的transform方法进行转换。
在自定义图片变换之前,我们来看一下Glide内置的图片变换,以centerCrop为例,它于我们给ImageView设置scaleType为centerCrop的展现效果是一样的
Glide.with(this).load("https://gank.io/images/25d3e3db2c1248bb917c09dc4f50a46f").centerCrop().into(iv)
跟进centerCrop()方法,看一下内部实现
public T centerCrop() {
return transform(DownsampleStrategy.CENTER_OUTSIDE, new CenterCrop());
}
这里实际上是传入了一个CenterCrop类对象,对Bitmap的转换封装在这个类里面,看一下这个类的源码
public class CenterCrop extends BitmapTransformation {
...
@Override
protected Bitmap transform(
@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
return TransformationUtils.centerCrop(pool, toTransform, outWidth, outHeight);
}
...
}
这里有一个BitmapPool对象,一般来说xxPool翻译成中文都是xx池,用作缓存和复用,BitmapPool就是缓存和复用Bitmap的。注意Transformation接口定义的tranform方法是没有BitmapPool参数的,所以我们来看一下CenterCrop的父类BitmapTransformation
public abstract class BitmapTransformation implements Transformation<Bitmap> {
@NonNull
@Override
public final Resource<Bitmap> transform(
@NonNull Context context, @NonNull Resource<Bitmap> resource, int outWidth, int outHeight) {
if (!Util.isValidDimensions(outWidth, outHeight)) {
throw new IllegalArgumentException(
"Cannot apply transformation on width: "
+ outWidth
+ " or height: "
+ outHeight
+ " less than or equal to zero and not Target.SIZE_ORIGINAL");
}
BitmapPool bitmapPool = Glide.get(context).getBitmapPool();
Bitmap toTransform = resource.get();
int targetWidth = outWidth == Target.SIZE_ORIGINAL ? toTransform.getWidth() : outWidth;
int targetHeight = outHeight == Target.SIZE_ORIGINAL ? toTransform.getHeight() : outHeight;
Bitmap transformed = transform(bitmapPool, toTransform, targetWidth, targetHeight);
final Resource<Bitmap> result;
if (toTransform.equals(transformed)) {
result = resource;
} else {
result = BitmapResource.obtain(transformed, bitmapPool);
}
return result;
}
protected abstract Bitmap transform(
@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight);
}
果然,BitmapTransformation重写了父类的transform方法,并且定义了一个新的带有BitmapPool参数的抽象transform方法,要求子类实现。这里面做了一些优化操作,所以我们后续也可以基于BitmapTransformation来定义我们的变换操作。
回过头来看CenterCrop,它的transform方法是调用TransformationUtils来完成变换的,我们看一下TransformationUtils的centerCrop方法
public static Bitmap centerCrop(
@NonNull BitmapPool pool, @NonNull Bitmap inBitmap, int width, int height) {
if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) { // 宽高和目标宽高一致就直接返回了
return inBitmap;
}
// From ImageView/Bitmap.createScaledBitmap.
final float scale;
final float dx;
final float dy;
Matrix m = new Matrix();
if (inBitmap.getWidth() * height > width * inBitmap.getHeight()) {
scale = (float) height / (float) inBitmap.getHeight();
dx = (width - inBitmap.getWidth() * scale) * 0.5f;
dy = 0;
} else {
scale = (float) width / (float) inBitmap.getWidth();
dx = 0;
dy = (height - inBitmap.getHeight() * scale) * 0.5f;
}
m.setScale(scale, scale);
m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
// 从池中获取一个Bitmap对象
Bitmap result = pool.get(width, height, getNonNullConfig(inBitmap));
// We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given.
TransformationUtils.setAlpha(inBitmap, result);
// 根据变换矩阵完成变换
applyMatrix(inBitmap, result, m);
return result;
}
通常我们进行Bitmap的变换是这样的
// 根据目标配置创建一个新的Bitmap
val targetBitmap = Bitmap.createBitmap(targetWidth, targetHeight, targetConfig)
val canvas = Canvas(targetBitmap)
// 进行变换
canvas.drawBitmap(srcBitmap, matrix, DEFAULT_PAINT)
// 回收旧的Bitmap
srcBitmap.recycle()
思考一下,这样做有什么问题?对于单个变换来说,其实没太大问题。但是如果需要频繁地加载大量的图片,比如微博等社交应用和图片浏览相关的应用,在滚动的过程中会频繁地分配和释放内存,就有可能会导致内存抖动。所以我们需要这个BitmapPool。
好了,了解了图片变换的实现流程之后,我们自己动手实现一个高斯模糊的变换效果:
Glide.with(this)
.load("https://gank.io/images/25d3e3db2c1248bb917c09dc4f50a46f")
.transform(object : BitmapTransformation() {
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
}
override fun transform(
pool: BitmapPool,
toTransform: Bitmap,
outWidth: Int,
outHeight: Int
): Bitmap {
val transform = TransformationUtils.fitCenter(pool, toTransform, outWidth, outHeight)
return FastBlur.blur(transform, 30, true)
}
})
.into(iv)
其中FastBlur来自这里,这个开源库实现了一些常用的变换,大家不妨看一看。
4、过渡动画
Glide同样既有内置过渡动画,也支持自定义过渡动画。先来看一下内置动画的使用,以cross fade为例:
Glide.with(this)
.load("https://gank.io/images/25d3e3db2c1248bb917c09dc4f50a46f")
.transition(DrawableTransitionOptions.withCrossFade(3000)) // 设置一个比较大的时间方便观察效果
.into(iv)
或者
Glide.with(this)
.asBitmap()
.load("https://gank.io/images/25d3e3db2c1248bb917c09dc4f50a46f")
.transition(BitmapTransitionOptions.withCrossFade(3000))
.into(iv)
这两种方法分别对应的加载成Drawable和Bitmap。transition方法接收一个定义过渡行为的TransitionOptions对象。TransitionOptions是一个抽象类,定义如下
public abstract class TransitionOptions<
CHILD extends TransitionOptions<CHILD, TranscodeType>, TranscodeType>
implements Cloneable {
private TransitionFactory<? super TranscodeType> transitionFactory = NoTransition.getFactory();
@NonNull
public final CHILD dontTransition() {
return transition(NoTransition.getFactory());
}
@NonNull
public final CHILD transition(int viewAnimationId) {
return transition(new ViewAnimationFactory<>(viewAnimationId));
}
@NonNull
public final CHILD transition(@NonNull ViewPropertyTransition.Animator animator) {
return transition(new ViewPropertyAnimationFactory<>(animator));
}
@NonNull
public final CHILD transition(
@NonNull TransitionFactory<? super TranscodeType> transitionFactory) {
this.transitionFactory = Preconditions.checkNotNull(transitionFactory);
return self();
}
@Override
public final CHILD clone() {
try {
return (CHILD) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
final TransitionFactory<? super TranscodeType> getTransitionFactory() {
return transitionFactory;
}
@SuppressWarnings("unchecked")
private CHILD self() {
return (CHILD) this;
}
}
可以看到虽然它是个抽象类,但是所有public方法都是final的,也就是不能被子类重写的。它定义了一个TransitionFactory属性,通过不断跟进可以发现这个TransitionFactory真正使用的地方是在SingleRequest#onResourceReady方法中
private void onResourceReady(
Resource<R> resource, R result, DataSource dataSource, boolean isAlternateCacheKey) {
...
isCallingCallbacks = true;
try {
...
if (!anyListenerHandledUpdatingTarget) {
Transition<? super R> animation = animationFactory.build(dataSource, isFirstResource);
target.onResourceReady(result, animation);
}
} finally {
isCallingCallbacks = false;
}
notifyLoadSuccess();
}
也就是在资源加载成功的时候,通过这个工厂类得到一个Transition对象,然后应用它定义的过渡。所以我们如果要自定义过渡动画,只需要定义我们自己的工厂类和对应的Transition子类即可。
TransitionOptions有两个子类BitmapTransitionOptions和DrawableTransitionOptions。我们以BitmapTransitionOptions为例,看一下它的源码,这里只列出上面使用的BitmapTransitionOptions.withCrossFade(3000)的相关的代码,其他的省略:
public final class BitmapTransitionOptions
extends TransitionOptions<BitmapTransitionOptions, Bitmap> {
public static BitmapTransitionOptions withCrossFade(int duration) {
return new BitmapTransitionOptions().crossFade(duration);
}
public BitmapTransitionOptions crossFade(int duration) {
return crossFade(new DrawableCrossFadeFactory.Builder(duration));
}
public BitmapTransitionOptions transitionUsing(
@NonNull TransitionFactory<Drawable> drawableCrossFadeFactory) {
return transition(new BitmapTransitionFactory(drawableCrossFadeFactory));
}
public BitmapTransitionOptions crossFade(@NonNull DrawableCrossFadeFactory.Builder builder) {
return transitionUsing(builder.build());
}
}
可以看到,最终只是构造了一个BitmapTransitionFactory对象,传递给了transition方法。而BitmapTransitionFactory实际上是封装了DrawableCrossFadeFactory,来看看DrawableCrossFadeFactory和接口类TransitionFactory吧
public interface TransitionFactory<R> {
Transition<R> build(DataSource dataSource, boolean isFirstResource);
}
public class DrawableCrossFadeFactory implements TransitionFactory<Drawable> {
private final int duration;
private final boolean isCrossFadeEnabled;
private DrawableCrossFadeTransition resourceTransition;
protected DrawableCrossFadeFactory(int duration, boolean isCrossFadeEnabled) {
this.duration = duration;
this.isCrossFadeEnabled = isCrossFadeEnabled;
}
@Override
public Transition<Drawable> build(DataSource dataSource, boolean isFirstResource) {
return dataSource == DataSource.MEMORY_CACHE
? NoTransition.<Drawable>get()
: getResourceTransition();
}
private Transition<Drawable> getResourceTransition() {
if (resourceTransition == null) {
resourceTransition = new DrawableCrossFadeTransition(duration, isCrossFadeEnabled);
}
return resourceTransition;
}
}
可以看到这里做了一个判断,如果是直接从内存加载的就不进行动画,否则才进行动画。DrawableCrossFadeFactory对应的Transition实现类为DrawableCrossFadeTransition,看看它的源码
public class DrawableCrossFadeTransition implements Transition<Drawable> {
private final int duration;
private final boolean isCrossFadeEnabled;
public DrawableCrossFadeTransition(int duration, boolean isCrossFadeEnabled) {
this.duration = duration;
this.isCrossFadeEnabled = isCrossFadeEnabled;
}
@Override
public boolean transition(Drawable current, ViewAdapter adapter) {
Drawable previous = adapter.getCurrentDrawable();
if (previous == null) {
previous = new ColorDrawable(Color.TRANSPARENT);
}
TransitionDrawable transitionDrawable =
new TransitionDrawable(new Drawable[] {previous, current});
transitionDrawable.setCrossFadeEnabled(isCrossFadeEnabled);
transitionDrawable.startTransition(duration);
adapter.setDrawable(transitionDrawable);
return true;
}
}
可以看到,这里也很简单地通过TransitionDrawable来实现了淡入淡出的过渡动画。所以总结自定义过渡动画要点:
- 自定义Transition子类
- 自定义对对应的TransitionFactory
接下来实战一下,我们来自定以一个缩放的过渡动画
class DrawableScaleFactory(
private val duration: Int = 500,
private val isScaleEnabled: Boolean = false
) : TransitionFactory<Drawable> {
private var resourceTransition: DrawableCrossFadeTransition? = null
override fun build(dataSource: DataSource, isFirstResource: Boolean): Transition<Drawable>? {
return if (dataSource == DataSource.MEMORY_CACHE) NoTransition.get() else getResourceTransition()
}
private fun getResourceTransition(): Transition<Drawable>? {
if (resourceTransition == null) {
resourceTransition = DrawableCrossFadeTransition(duration, isScaleEnabled)
}
return resourceTransition
}
}
class DrawableCrossFadeTransition(private val duration: Int, private val isScaleEnabled: Boolean) : Transition<Drawable> {
override fun transition(current: Drawable, adapter: ViewAdapter): Boolean {
val animator = ValueAnimator.ofFloat(0f, 1f)
animator.duration = duration.toLong()
animator.repeatCount = 0
val d = AnimatorDrawable(current, animator)
adapter.setDrawable(d)
return true
}
}
class AnimatorDrawable(val d: Drawable, val animator : ValueAnimator) : Drawable(), Animatable, ValueAnimator.AnimatorUpdateListener {
private val originWidth: Int = d.intrinsicWidth
private val originHeight: Int = d.intrinsicHeight
init {
animator.addUpdateListener(this)
animator.start()
}
override fun start() {
if(!animator.isRunning) animator.start()
}
override fun stop() {
animator.cancel()
}
override fun isRunning() = animator.isRunning
override fun draw(canvas: Canvas) {
d.draw(canvas)
}
override fun setAlpha(alpha: Int) {
d.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
d.colorFilter = colorFilter
}
override fun getOpacity(): Int {
return d.opacity
}
override fun onAnimationUpdate(animation: ValueAnimator) {
val scale = animation.animatedValue as Float
val newWidth = (scale * originWidth).roundToInt()
val newHeight =( scale * originHeight).roundToInt()
val left = (originWidth - newWidth) / 2
val top = (originHeight - newHeight) / 2
d.setBounds(left, top, originWidth - left, originHeight - top)
invalidateSelf()
}
}
使用
Glide.with(this)
.load("https://gank.io/images/25d3e3db2c1248bb917c09dc4f50a46f")
.transition(DrawableTransitionOptions.with(DrawableScaleFactory()))
.into(iv)
5、缓存
要控制Glide缓存行为,如下
Glide.with(this)
.load("https://gank.io/images/25d3e3db2c1248bb917c09dc4f50a46f")
.skipMemoryCache(true) // 是否跳过内存缓存
.diskCacheStrategy(DiskCacheStrategy.ALL) // 磁盘缓存策略
.into(iv)
但是有时候,这样的缓存控制还不能满足我们的需求。Glide默认使用URL等一系列参数通过计算得出一个值作为缓存的key的。假设我们请求图片的时候是需要带一个token参数的,如
https://gank.io/images/25d3e3db2c1248bb917c09dc4f50a46f?token=123456
每次登录后服务器给我们分配的token是不一样的,但是我们要加载的图片是一样的。这时候如果想要避免重复加载,就需要我们自定义缓存的key。缓存的key的计算参数有很多,但是我们能控制的主要有两个:
- URL
- signature
前者,我们可以把去掉token后的URL作为key的计算参数;后者,我们可以通过它增加一些额外的key计算的参数。示例:
Glide.with(this)
.load(object : GlideUrl("https://gank.io/images/25d3e3db2c1248bb917c09dc4f50a46f?token=123456") {
override fun getCacheKey(): String {
return "https://gank.io/images/25d3e3db2c1248bb917c09dc4f50a46f"
}
}) // 可以自定义一个类,这里为了方便展示直接随手写成匿名类
.signature(DateKey())
.into(iv)
class DateKey : Key {
val date = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(System.currentTimeMillis())
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(date.toByteArray())
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DateKey
if (date != other.date) return false
return true
}
override fun hashCode(): Int {
return date?.hashCode() ?: 0
}
}
需要注意的是,Key的子类应该重写equals和hashCode方法。
6、自定义配置
Glide允许我们通过自定义Module来修改一些自定义配置信息,有以下两种AppGlideModule和LibraryGlideModule两种,其中AppModule其实也是继承自LibraryGlideModule的。示例
@GlideModule
class MyLibModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry)
registry.replace(GlideUrl::class.java, InputStream::class.java, VolleyUrlLoader.Factory(context))
Log.d("Module", "registerComponents...")
}
}
@GlideModule
class MyAppModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
super.applyOptions(context, builder)
builder.setLogLevel(Log.VERBOSE)
Log.d("Module", "applyOptions...")
}
}
通过LibraryGlideModule我们可以注册一些自定义组件等,通过AppGlideModule我们可以对缓存区大小、日志级别以及全局异常等做出一些设置和处理。
一般用得不多,这里只简单介绍。
7、RecyclerView防错位
接下来看一下Glide是怎么做到在RecyclerView和ListView等组件中做到不错位的。大部分情况下,我们使用Glide只需要简单的一句
Glide.with(this).load(uri).into(imageView)
所以来看看into方法
public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) {
Util.assertMainThread(); // 只能在主线程调用
...
return into(
glideContext.buildImageViewTarget(view, transcodeClass),
/*targetListener=*/ null,
requestOptions,
Executors.mainThreadExecutor());
}
这里把ImageView封装成了ImageViewTarget,调用了另外一个into重载方法
private <Y extends Target<TranscodeType>> Y into(
@NonNull Y target,
@Nullable RequestListener<TranscodeType> targetListener,
BaseRequestOptions<?> options,
Executor callbackExecutor) {
...
Request request = buildRequest(target, targetListener, options, callbackExecutor);
Request previous = target.getRequest();
if (request.isEquivalentTo(previous)
&& !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
// 如果前后两次请求是一样的且请求还没有开始,那就开始轻轻
if (!Preconditions.checkNotNull(previous).isRunning()) {
previous.begin();
}
return target;
}
// 清除之前关联的请求
requestManager.clear(target);
// 关联新的请求
target.setRequest(request);
requestManager.track(target, request);
return target;
}
这里的逻辑非常简单,接下来让我们看看ImageViewTarget是怎么构建的,也就是GlideContext#buildImageViewTarget方法
@NonNull
public <X> ViewTarget<ImageView, X> buildImageViewTarget(
@NonNull ImageView imageView, @NonNull Class<X> transcodeClass) {
return imageViewTargetFactory.buildTarget(imageView, transcodeClass);
}
它调用了ImageViewTargetFactory来执行具体的创建
public class ImageViewTargetFactory {
@NonNull
@SuppressWarnings("unchecked")
public <Z> ViewTarget<ImageView, Z> buildTarget(
@NonNull ImageView view, @NonNull Class<Z> clazz) {
if (Bitmap.class.equals(clazz)) {
return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view);
} else if (Drawable.class.isAssignableFrom(clazz)) {
return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view);
} else {
throw new IllegalArgumentException(
"Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)");
}
}
}
这里是直接根据不同的class来new不同的Target子类。那么如果每次都是直接new的,在into方法中
Request previous = target.getRequest();
这一句的previous岂不是永远是null?无论是BitmapImageViewTarget还是DrawableImageViewTarget都是继承自ImageViewTarget,而ImageViewTarget又继承自ViewTarget,在ViewTarget中getRequest方法被重写了
public Request getRequest() {
Object tag = getTag();
Request request = null;
if (tag != null) {
if (tag instanceof Request) {
request = (Request) tag;
} else {
throw new IllegalArgumentException(
"You must not call setTag() on a view Glide is targeting");
}
}
return request;
}
private Object getTag() {
return view.getTag(tagId);
}
同样地,setRequest方法也被重写了。它们对View进行了tag的存取,所以只要传入的是同一个View,无论这个ViewTarget是不是新new的,一样是可以取到之前关联的Request的。另外,对应的Request对象也持有了Target的引用,只有Request没有取消,才会在加载完成的时候设置Request的结果到Target上。