跳转至

Glide v4 源码解析(四)

Tip

本系列文章参考3.7.0版本的guolin - Glide最全解析,并按此思路结合4.9.0版本源码以及使用文档进行更新。
Glide v4.9.0
中文文档
英文文档🚀🚀


本章主要内容为Target的相关知识、RequestBuilder的高级API。

1. Target

在本系列文章的第二章中比较详细地介绍了Glide.with(xx).load(xx).into(xx)的过程。回想一下,在into(ImageView)过程中(Link),会将ImageView包装成为一个ViewTarget类。如果调用过asBitmap()方法,那么此处会是BitmapImageViewTarget,否则都将会是DrawableImageViewTargetBitmapImageViewTargetDrawableImageViewTarget除了setResource方法中调用的设置图片的API不同外,没有任何区别。

ImageViewTargetFactory.java

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)");
    }
  }
}

DrawableImageView的继承链如下:DrawableImageView -> ImageViewTarget -> ViewTarget -> BaseTarget -> Target

  • Target是一个继承了LifecycleListener接口的接口类,该类提供了资源加载过程中的回调操作。
    典型的生命周期为onLoadStarted -> onResourceReady/onLoadFailed -> onLoadCleared,但不保证所有的都是这样。如果资源在内存中或者由于model为null而加载失败,onLoadStarted不会被调用。同样,如果target不会清除,那么onLoadCleared方法也不会被调用。
  • BaseTarget是一个实现了Target接口的抽象类。
    该类实现了setRequest(Request)getRequest()两个方法,其他方法相当于适配器模式的实现。
  • ViewTarget
    该类虽然继承了BaseTarget类,但其重写了setRequest(Request)getRequest()两个方法,这两个方法会调用View.setTag方法将Request对象传入。

    In addition, for ViewTargets only, you can pass in a new instance to each load or clear call and allow Glide to retrieve information about previous loads from the Views tags
    This will not work unless your Target extends ViewTarget or implements setRequest() and getRequest() in a way that allows you to retrieve previous requests in new Target instances.
    Cancellation and re-use

  • ImageViewTarget
    该类的作用就是在加载的生命周期回调中给ImageView设置对应的资源。但由于加载成功后返回的资源可能是Bitmap或者Drawable,所以这个不确定类型的加载由setResource抽象方法声明,待子类BitmapImageViewTargetDrawableImageViewTarget实现。
  • DrawableImageViewTarget
    继承了ImageViewTarget类,唯一的作用就是实现setResource(Drawable)方法。

在了解了DrawableImageViewTarget以及相关的类之后,我们看一下其他的Target。下面是Glide v4中出现的所有的Target

Glide v4中所有的Target

虽然,Target很多,但是我们自定义只需要继承CustomViewTarget或者CustomTarget就行了。

为什么要继承CustomViewTarget而不是ViewTarget?
ViewTarget已经被标记为废弃了,建议我们使用CustomViewTarget。这是因为,如果子类没有实现ViewTarget.onLoadCleared方法,将会导致被回收的bitmap仍然被UI所引用,从而导致崩溃。而CustomViewTarget.onLoadCleared方法是final类型的,并且提供了一个抽象方法onResourceCleared强制我们实现。除此之外,两个类基本没有任何区别。

为什么要继承CustomTarget而不是SimpleTarget?
原因同上

下面举一个实际例子,在某些场景下,此时我们需要获取到加载成功后的Bitmap对象(虽然这样有点蠢,因为有其他更好的方式):

Glide.with(this)
    .asBitmap()
    .load(file)
    .into(object : CustomTarget<Bitmap>() {
        override fun onResourceReady(
            resource: Bitmap,
            transition: Transition<in Bitmap>?
        ) {
            ivFace.setImageBitmap(resource)
        }

        override fun onLoadCleared(placeholder: Drawable?) {
            ivFace.setImageDrawable(placeholder)
        }
    })

2. RequestBuilder高级API

在了解了Target之后,我们再看看RequestBuilder中高级一点的API。

下面这些都是Target的应用:

  • preload
    将资源预加载到缓存中
  • submit
    返回一个Future对象,其get()方法会阻塞住,所以需要在后台线程中调用
  • downloadOnly
    下载原始的无修改的data文件。
    内部调用的是**修改过配置**的into/submit方法,但RequestBuilder.downloadOnly方法已经被废弃;建议采用RequestManagerdownloadOnly()方法和into/submit方法

此外还有还需要注意的一个API:

  • listener/addListener

2.1 preload

preload的重载方法如下:

/**
  * Preloads the resource into the cache using the given width and height.
  *
  * <p> Pre-loading is useful for making sure that resources you are going to to want in the near
  * future are available quickly. </p>
  *
  * @param width  The desired width in pixels, or {@link Target#SIZE_ORIGINAL}. This will be
  *               overridden by
  *               {@link com.bumptech.glide.request.RequestOptions#override(int, int)} if
  *               previously called.
  * @param height The desired height in pixels, or {@link Target#SIZE_ORIGINAL}. This will be
  *               overridden by
  *               {@link com.bumptech.glide.request.RequestOptions#override(int, int)}} if
  *               previously called).
  * @return A {@link Target} that can be used to cancel the load via
  * {@link RequestManager#clear(Target)}.
  * @see com.bumptech.glide.ListPreloader
  */
@NonNull
public Target<TranscodeType> preload(int width, int height) {
  final PreloadTarget<TranscodeType> target = PreloadTarget.obtain(requestManager, width, height);
  return into(target);
}

/**
  * Preloads the resource into the cache using {@link Target#SIZE_ORIGINAL} as the target width and
  * height. Equivalent to calling {@link #preload(int, int)} with {@link Target#SIZE_ORIGINAL} as
  * the width and height.
  *
  * @return A {@link Target} that can be used to cancel the load via
  * {@link RequestManager#clear(Target)}
  * @see #preload(int, int)
  */
@NonNull
public Target<TranscodeType> preload() {
  return preload(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
}

注意,在注释中出现了一个ListPreload类,该类是在ListView中做item预加载的一个工具类,使用方法为AbsListView#setOnScrollListener(android.widget.AbsListView.OnScrollListener)。该类代码很简单,要点就是在滚动时计算需要预处理的item。

这么好用,那我要是RecyclerView怎么办?Glide也提供了RecyclerView的版本,不过需要添加新的依赖recyclerview-integration,详情可以查看文档INTEGRATION LIBRARIES - RecyclerView

我们可以看到,在preload的实现中关键点就在于PreloadTarget类。该类实现非常简单,就是在onResourceReady回调发生后,经过Handler中转,最后由构造参数之一的RequestManager对象clear掉。代码如下:

PreloadTarget源代码
/**
 * A one time use {@link com.bumptech.glide.request.target.Target} class that loads a resource into
 * memory and then clears itself.
 *
 * @param <Z> The type of resource that will be loaded into memory.
 */
public final class PreloadTarget<Z> extends SimpleTarget<Z> {
  private static final int MESSAGE_CLEAR = 1;
  private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Callback() {
    @Override
    public boolean handleMessage(Message message) {
      if (message.what == MESSAGE_CLEAR) {
        ((PreloadTarget<?>) message.obj).clear();
        return true;
      }
      return false;
    }
  });

  private final RequestManager requestManager;

  /**
   * Returns a PreloadTarget.
   *
   * @param width  The width in pixels of the desired resource.
   * @param height The height in pixels of the desired resource.
   * @param <Z>    The type of the desired resource.
   */
  public static <Z> PreloadTarget<Z> obtain(RequestManager requestManager, int width, int height) {
    return new PreloadTarget<>(requestManager, width, height);
  }

  private PreloadTarget(RequestManager requestManager, int width, int height) {
    super(width, height);
    this.requestManager = requestManager;
  }

  @Override
  public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
    HANDLER.obtainMessage(MESSAGE_CLEAR, this).sendToTarget();
  }

  @SuppressWarnings("WeakerAccess")
  @Synthetic void clear() {
    requestManager.clear(this);
  }
}

2.2 submit

submit的两个重载方法如下:

/**
  * Returns a future that can be used to do a blocking get on a background thread.
  *
  * <p>This method defaults to {@link Target#SIZE_ORIGINAL} for the width and the height. However,
  * since the width and height will be overridden by values passed to {@link
  * RequestOptions#override(int, int)}, this method can be used whenever {@link RequestOptions}
  * with override values are applied, or whenever you want to retrieve the image in its original
  * size.
  *
  * @see #submit(int, int)
  * @see #into(Target)
  */
@NonNull
public FutureTarget<TranscodeType> submit() {
  return submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL);
}

/**
  * Returns a future that can be used to do a blocking get on a background thread.
  *
  * @param width  The desired width in pixels, or {@link Target#SIZE_ORIGINAL}. This will be
  *               overridden by
  *               {@link com.bumptech.glide.request.RequestOptions#override(int, int)} if
  *               previously called.
  * @param height The desired height in pixels, or {@link Target#SIZE_ORIGINAL}. This will be
  *               overridden by
  *               {@link com.bumptech.glide.request.RequestOptions#override(int, int)}} if
  *               previously called).
  */
@NonNull
public FutureTarget<TranscodeType> submit(int width, int height) {
  final RequestFutureTarget<TranscodeType> target = new RequestFutureTarget<>(width, height);
  return into(target, target, Executors.directExecutor());
}

由于方法会生成一个RequestFutureTarget对象,而其getSize的实现就是构造参数。所以,此处的值会覆盖掉RequestOptions设置的值。

submit之后生成了一个RequestFutureTarget对象,调用该对象的get方法可以在资源加载成功后立即获得资源对象,在获得之前会阻塞,所以get方法需要在后台线程中执行,否则会报错。

RequestFutureTarget的示例代码如下:

RequestFutureTarget的示例代码
FutureTarget<File> target = null;
RequestManager requestManager = Glide.with(context);
try {
  target = requestManager
     .downloadOnly()
     .load(model)
     .submit();
  File downloadedFile = target.get();
  // ... do something with the file (usually throws IOException)
} catch (ExecutionException | InterruptedException | IOException e) {
  // ... bug reporting or recovery
} finally {
  // make sure to cancel pending operations and free resources
  if (target != null) {
    target.cancel(true); // mayInterruptIfRunning
  }
}

2.3 downloadOnly

downloadOnly内部调用的是**修改过配置**的into/submit方法,但downloadOnly方法已经被废弃;建议采用RequestManagerdownloadOnly()方法和into/submit方法。
实际上RequestBuilder.downloadOnly方法与RequestManager.downloadOnly()RequestBuilder.into/submit方法组合没有什么区别。

两处代码如下,各位可自行对比:

RequestBuilder.downloadOnly
@Deprecated
@CheckResult
public <Y extends Target<File>> Y downloadOnly(@NonNull Y target) {
  return getDownloadOnlyRequest().into(target);
}

@Deprecated
@CheckResult
public FutureTarget<File> downloadOnly(int width, int height) {
  return getDownloadOnlyRequest().submit(width, height);
}

@NonNull
@CheckResult
protected RequestBuilder<File> getDownloadOnlyRequest() {
  return new RequestBuilder<>(File.class, this).apply(DOWNLOAD_ONLY_OPTIONS);
}
RequestManager.downloadOnly、RequestBuilder.into/submit
@NonNull
public <Y extends Target<TranscodeType>> Y into(@NonNull Y target) {
  return into(target, /*targetListener=*/ null, Executors.mainThreadExecutor());
}

public FutureTarget<TranscodeType> submit(int width, int height) {
  final RequestFutureTarget<TranscodeType> target = new RequestFutureTarget<>(width, height);
  return into(target, target, Executors.directExecutor());
}

@NonNull
@CheckResult
public RequestBuilder<File> downloadOnly() {
  return as(File.class).apply(DOWNLOAD_ONLY_OPTIONS);
}

所以,这里的DOWNLOAD_ONLY_OPTIONS才是downloadOnly的精髓,我们看看该变量的值:

protected static final RequestOptions DOWNLOAD_ONLY_OPTIONS =
    new RequestOptions().diskCacheStrategy(DiskCacheStrategy.DATA).priority(Priority.LOW)
        .skipMemoryCache(true);

果然是下载的是原始的无修改的data资源。

2.4 listener/addListener

listeneraddListener不同之处在于,前者只会保留当前的Listener,而后者会保留之前的Listener。

@NonNull
@CheckResult
@SuppressWarnings("unchecked")
public RequestBuilder<TranscodeType> listener(
    @Nullable RequestListener<TranscodeType> requestListener) {
  this.requestListeners = null;
  return addListener(requestListener);
}

@NonNull
@CheckResult
public RequestBuilder<TranscodeType> addListener(
    @Nullable RequestListener<TranscodeType> requestListener) {
  if (requestListener != null) {
    if (this.requestListeners == null) {
      this.requestListeners = new ArrayList<>();
    }
    this.requestListeners.add(requestListener);
  }
  return this;
}

这些listener会在资源记载失败或者成功的时候被调用,代码如下:

SingleRequest中关于requestListeners的代码
private synchronized void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {
  // We must call isFirstReadyResource before setting status.
  boolean isFirstResource = isFirstReadyResource();
  status = Status.COMPLETE;
  this.resource = resource;

  isCallingCallbacks = true;
  try {
    boolean anyListenerHandledUpdatingTarget = false;
    if (requestListeners != null) {
      for (RequestListener<R> listener : requestListeners) {
        anyListenerHandledUpdatingTarget |=
            listener.onResourceReady(result, model, target, dataSource, isFirstResource);
      }
    }
    anyListenerHandledUpdatingTarget |=
        targetListener != null
            && targetListener.onResourceReady(result, model, target, dataSource, isFirstResource);

    if (!anyListenerHandledUpdatingTarget) {
      Transition<? super R> animation =
          animationFactory.build(dataSource, isFirstResource);
      target.onResourceReady(result, animation);
    }
  } finally {
    isCallingCallbacks = false;
  }

  notifyLoadSuccess();
}

private synchronized void onLoadFailed(GlideException e, int maxLogLevel) {
  stateVerifier.throwIfRecycled();
  e.setOrigin(requestOrigin);

  loadStatus = null;
  status = Status.FAILED;

  isCallingCallbacks = true;
  try {
    //TODO: what if this is a thumbnail request?
    boolean anyListenerHandledUpdatingTarget = false;
    if (requestListeners != null) {
      for (RequestListener<R> listener : requestListeners) {
        anyListenerHandledUpdatingTarget |=
            listener.onLoadFailed(e, model, target, isFirstReadyResource());
      }
    }
    anyListenerHandledUpdatingTarget |=
        targetListener != null
            && targetListener.onLoadFailed(e, model, target, isFirstReadyResource());

    if (!anyListenerHandledUpdatingTarget) {
      setErrorPlaceholder();
    }
  } finally {
    isCallingCallbacks = false;
  }

  notifyLoadFailed();
}

调用逻辑是这样:在requestListeners集合、targetListener中依次调用对应的回调,找到第一个能够处理的(返回true),后面的就不再调用。
同时,如果有一个回调返回了true,那么资源的对应方法会被拦截:

  1. 对于onResourceReady方法来说,TargetonResourceReady方法不会被回调
  2. 对于onLoadFailed方法来说,setErrorPlaceholder调用不会调用,即不会显示任何失败的占位符

评论