详解 Glide框架(二):Target的构建过程和图片的请求策略

1,178 阅读9分钟

Glide系列第一篇最后我说下一期要探索Glide中的缓存策略,但是在今天这个内容中我不得不食言了。原以为Glide初始化之后,除了缓存策略就没有太多有趣的点值得探讨,但是深入一看,确实有一些值得我们学习的思想内容,所以,今天这篇的内容就像题目描述的那样,我们一起探索下Glide中图片通过Request请求的设计方式。

还是以一句代码为例,我们先概括Glide在执行过程中存在的三个阶段。

Glide.with(context).load(url).asBitmap().into(imageView)

  • 第一步:Glide.with(context)Glide的初始化;
  • 第二步:asBitmap().load(url)参数配置,包括资源来源,资源的类型等信息;
  • 第三部:into(imageView),真正将之前设置的参数应用于图片载体Target的构建,图片的请求,缓存策略的使用等。

上一篇文章《详解 Glide框架(一):Glide的初始化流程》中我们详细介绍了第一步,包括其单例的初始化,以及选择合适的Context作为生命周期的管理容器等内容。接下来,我们会简单介绍下第二步的内容,真正要重点了解的其实是第三步中的操作。

从RequestManager到RequestBuilder

经过Glide.with()的一顿操作之后,我们得到了RequestManager这个类。 RequestManager 对外暴露的最主要能力之一,就是创建合适资源类型的RequestBuilder,如asBitmap()会得到 RequestBuilder< Bitmap >。


  public RequestBuilder<Bitmap> asBitmap() {
    return as(Bitmap.class).apply(DECODE_TYPE_BITMAP);
  }
  
  public <ResourceType> RequestBuilder<ResourceType> as(Class<ResourceType> resourceClass) {
    return new RequestBuilder<>(glide, this, resourceClass);
    //构造方法中,this指代的就是RequestManager,注意,它并不是单例,而是与RequestManagerFragment相关联的实例。
    //泛型resourceClass则是举例中的Bitmap.class。同理,如果调用的是asGif或asFile,则是对应的class类型
  }
  

当然,asXXX方法是获取RequestBuilder的起始方法,只有asXXX调用之后我们才能够唯一确定resourceType到底是什么类型。因此我们得知,asXXX和load()等方法的顺序是不能颠倒的。从源码也可以看出,如果没有asXXX()的调用,则默认为asDrawable()方法,这里就不贴了。

下面分析RequestBuilder< Bitmap >的执行内容。

说实话,建造者模式的RequestBuilder,其方法并不是多,而且大多数是重载方法。其中包含我们最常用的load()into()。 load()的众多重载方法最后都执行loadGeneric():

private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {
    this.model = model;
    isModelSet = true;
    return this;
  }

作为建造者模式的典型行为,它记录了传入的图片源信息,并设置信息源标记位

最后一步,则是调用into(),开始最后的流程。

into(),最后的冲刺

into()这个方法有点像典型的建造者模式中的最后一步,build()。我们暂且可以这么理解。对于into(imageView)来说,最后会调用以下这个方法:

  private <Y extends Target<TranscodeType>> Y into(@NonNull Y target, RequestOptions options) {
    Util.assertMainThread();
    Preconditions.checkNotNull(target);
    if (!isModelSet) {//检查图片源信息是否设置
      throw new IllegalArgumentException("You must call #load() before calling #into()");
    }

    options = options.autoClone();
    //第1件事儿
    Request request = buildRequest(target, options);
	
    Request previous = target.getRequest();
    
    //第2件事儿
    if (request.isEquivalentTo(previous)) {
      request.recycle();
      // If the request is completed, beginning again will ensure the result is re-delivered,
      // triggering RequestListeners and Targets. If the request is failed, beginning again will
      // restart the request, giving it another chance to complete. If the request is already
      // running, we can let it continue running without interruption.
      if (!Preconditions.checkNotNull(previous).isRunning()) {
        previous.begin();
      }
      return target;
    }
    
    //第3件事儿
    requestManager.clear(target);
    target.setRequest(request);
    //第4件事儿
    requestManager.track(target, request);

    return target;
  }

into的整体过程做了四件事儿:

  1. 获得图片请求的Request,建造或者复用
  2. 若拿到的request是复用的,则检查request状态,若没有在执行,则重新执行,previous.begin(),否则(处于执行中),就当无事发生,返回target,说明正处于最后阶段的执行过程中。
  3. 若2条件不满足,request无法复用,则用requestManager清理target的请求,重新设置新的请求target.setRequest(request)
  4. 通过requestManager启动新的请求并监控。

建造Request

在开篇举例的参数设置情况下,target就是ImageView,requestOption走的是default。我们没有在构建参数的时候调用thumbnail( @Nullable RequestBuilder<TranscodeType> thumbnailRequest)thumbnail(float sizeMultiplier), 也就不存在缩略图相关请求。那么最终调用到buildRequest流程就比较简单。如下是核心代码:

  private Request buildRequest(Target<TranscodeType> target, RequestOptions requestOptions) {
    return buildRequestRecursive(target, null, transitionOptions, requestOptions.getPriority(),
        requestOptions.getOverrideWidth(), requestOptions.getOverrideHeight(), requestOptions);
  }

  private Request buildRequestRecursive(Target<TranscodeType> target,
      @Nullable ThumbnailRequestCoordinator parentCoordinator,
      TransitionOptions<?, ? super TranscodeType> transitionOptions,
      Priority priority, int overrideWidth, int overrideHeight, RequestOptions requestOptions) {
    if (thumbnailBuilder != null) { //举例中我们没有设置缩略图请求,因此不走该分支。
      // Recursive case: contains a potentially recursive thumbnail request builder.
      if (isThumbnailBuilt) {
        throw new IllegalStateException("You cannot use a request as both the main request and a "
            + "thumbnail, consider using clone() on the request(s) passed to thumbnail()");
      }

      TransitionOptions<?, ? super TranscodeType> thumbTransitionOptions =
          thumbnailBuilder.transitionOptions;

      // Apply our transition by default to thumbnail requests but avoid overriding custom options
      // that may have been applied on the thumbnail request explicitly.
      if (thumbnailBuilder.isDefaultTransitionOptionsSet) {
        thumbTransitionOptions = transitionOptions;
      }

      Priority thumbPriority = thumbnailBuilder.requestOptions.isPrioritySet()
          ? thumbnailBuilder.requestOptions.getPriority() : getThumbnailPriority(priority);

      int thumbOverrideWidth = thumbnailBuilder.requestOptions.getOverrideWidth();
      int thumbOverrideHeight = thumbnailBuilder.requestOptions.getOverrideHeight();
      if (Util.isValidDimensions(overrideWidth, overrideHeight)
          && !thumbnailBuilder.requestOptions.isValidOverride()) {
        thumbOverrideWidth = requestOptions.getOverrideWidth();
        thumbOverrideHeight = requestOptions.getOverrideHeight();
      }

      ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
      Request fullRequest = obtainRequest(target, requestOptions, coordinator,
          transitionOptions, priority, overrideWidth, overrideHeight);
      isThumbnailBuilt = true;
      // Recursively generate thumbnail requests.
      Request thumbRequest =
          thumbnailBuilder.buildRequestRecursive(
              target,
              coordinator,
              thumbTransitionOptions,
              thumbPriority,
              thumbOverrideWidth,
              thumbOverrideHeight,
              thumbnailBuilder.requestOptions);
      isThumbnailBuilt = false;
      coordinator.setRequests(fullRequest, thumbRequest);
      return coordinator;
    } else if (thumbSizeMultiplier != null) {//举例中我们没有设置缩略图重载方法,因此也不走该分支。
      // Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse.
      ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
      Request fullRequest = obtainRequest(target, requestOptions, coordinator, transitionOptions,
          priority, overrideWidth, overrideHeight);
      RequestOptions thumbnailOptions = requestOptions.clone()
          .sizeMultiplier(thumbSizeMultiplier);

      Request thumbnailRequest = obtainRequest(target, thumbnailOptions, coordinator,
          transitionOptions, getThumbnailPriority(priority), overrideWidth, overrideHeight);

      coordinator.setRequests(fullRequest, thumbnailRequest);
      return coordinator;
    } else {//最基本的情况分支:无缩略图流程。
      // Base case: no thumbnail.
      return obtainRequest(target, requestOptions, parentCoordinator, transitionOptions, priority,
          overrideWidth, overrideHeight);
    }
  }

用尾递归的方式构建Request执行链

由于举例的行为过于单一,我们忽略了以上方法中的缩略图请求处理,也就相当于将整个递归加载的方式转换为了“只有一层的递归调用”。但在诸多的应用场景中,尤其是toC的很多场景,为了用户体验,我们需要尽快展示出缩略图,然后在替换为原图。不得不说,从官方注释也好,从方法命名上看也好,这个分支的水都是挺深的。不要以为这个分支逻辑就单单是通过一个叫ThumbnailRequestCoordinator的类来管理thumbnail和full两个图片的请求之间的协调。频繁出现的Recursive:递归就已经说明它在逻辑上的复杂度了。 因此我们要以递归的思想再去细看buildRequestRecursive()方法。这个方法抽象一下,其实就是以下伪代码逻辑(你品,你细品):

   private fun buildRequestBuilderRecursive() {
        if(hasInnerRequestBuilder()) {//这个说的就是通过thumbnail()设置进来的RequestBuilder。
            val innerRequestBuilder = getinnerRequestBuilder()
            innerRequestBuilder.buildRequestBuilderRecursive()
        } else {
            buildSingleRequest()
        }
    }

在源码中,Request thumbRequest = thumbnailBuilder.buildRequestRecursive(target, coordinator,...)这句的意思,是说处于当前的RequestBuild中,要想构建这一层的coordinator,就必须先构建好它递归下层的request。因此从request的构建顺序来看,是由内向外依次构建,最终返回最外层的Request(可能是一个SingleRequest,也可能是个Coordinator,如上面类图)。而递归最深处的那个Request,一定是一个SingleRequest,因为它走到了终止条件:

return obtainRequest(target, requestOptions, parentCoordinator, transitionOptions, priority,overrideWidth, overrideHeight);

到此,我们先不追究obtainRequest中的细节,我们整体的Request构建流程已经完成。

通过Target绑定Request

通过into(imageView)传入的ImageView,会通过ImageViewFactory.build()方法,根据as()制定的图像类型(这里是Bitmap)生成对应的ImageViewTarget对象,并内部持有ImageView。

public class ImageViewTargetFactory {

  @SuppressWarnings("unchecked")
  public <Z> Target<Z> buildTarget(ImageView view, Class<Z> clazz) {
    if (Bitmap.class.equals(clazz)) {//根据不同的图片类型,生成不同的Target对象
      return (Target<Z>) new BitmapImageViewTarget(view);
    } else if (Drawable.class.isAssignableFrom(clazz)) {
      return (Target<Z>) new DrawableImageViewTarget(view);
    } else {
      throw new IllegalArgumentException(
          "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)");
    }
  }
}

之后再调用target.setRequest()的过程中,其实是调用了ImageView.setTag(request)方法,将request直接作为Tag塞入了View中。

个人觉得这应该算是一个设计上的失误。曾经在使用的过程中就因为已经占用了View的tag,导致了使用Glide加载的时候崩溃。

启动Request链

用首递归的方式启动加载,以短路的方式完成替换

buildRequestRecursive()方法源码中,我们会发现一个代理类:ThumbnailRequestCoordinator。ThumbnailRequestCoordinator对象来说,thumb和full这两个Request是成对儿存在的。毕竟它的作用就是来协调他们两个的请求过程的。而它所持有的coordinator,来自递归的上一层,存在“parent”的含义(果不其然,在4.11.0版本中,该变量被命名为了parent),是用于执行短路逻辑的。ThumbnailRequestCoordinator类图如下所示:

ThumbnailCoordinator类图 因此从Request的角度来看,无论是SingleRequest还是ThumbnailRequestCoordinator,它都具备请求的能力。结合into()方法中的第四件事,我们可以了解到它的请求顺序:

第四件事儿:requestManager.track(target, request);

  public void runRequest(Request request) {
    requests.add(request);
    if (!isPaused) {
      request.begin();//主流程
    } else {
      pendingRequests.add(request);
    }
  }

在注释了“主流程”的这一行,真正的请求行动开始了。这里的request,是最外层的Request,它所持有的full,就是最终要展示的图片。接下来我们向里看:

@Override
  public void begin() {
    isRunning = true;
    if (!thumb.isRunning()) {
      thumb.begin();
    }
    if (isRunning && !full.isRunning()) {
      full.begin();
    }
  }

Request的启动是同时进行的,当前层的thumb则作为更深一层的full使用,而深一层又有自己的thumb。各个请求以首递归的方式依次启动。在这样的启动流程李,不免会生出一个巨大的疑问:既然最外层Request的full才是最终我们要展示的图片资源,而所有Request又是同时启动的,那如果full先于所有的深层thumbnail请求得到,该怎么办? 一起看下ThumbnailRequestCoordinator中的另外两个方法:

@Override
  public boolean canSetImage(Request request) {//判断传入的request是否有资格设置给Target。
    return parentCanSetImage() && (request.equals(full) || !full.isResourceSet());
  }

  private boolean parentCanSetImage() {//判断上层节点是否有资格
    return coordinator == null || coordinator.canSetImage(this);
  }

这两个方法说的其实就是在递归过程中,如何判断自身节点的有效性。持有上层节点的引用,能够首先调用上层节点的有效性,若parentCanSetImage,则再判断自身的有效性。因为越外层的Request获取的结果,优先级越高,因此,若上层都不具备设置图片的资格,那就说明,具备更高优先级的图片已经被设置进来,当前这条递归线路就到此为止,即便自身请求成功,这个资源也用不上了,因此后面还有释放资源的逻辑。这就是我们说的短路

最后,为了加深理解,提供这样一个例子,感兴趣的同学可以尝试下看看效果(为了调用明显起见,将三个requestBuilder加载的图片设置为不同的网络图片):

	val requestBuilder2 = Glide.with(this).asBitmap()
                .load("http://img02.tooopen.com/images/20160509/tooopen_sy_161967094653.jpg")
        val requestBuilder1 = Glide.with(this).asBitmap()
                .load("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1602580000631&di=1d3ef270d62405a618a96651f5a48cc7&imgtype=0&src=http%3A%2F%2Ft8.baidu.com%2Fit%2Fu%3D4132727200%2C2237246567%26fm%3D79%26app%3D86%26f%3DJPEG%3Fw%3D1280%26h%3D582")
                .thumbnail(requestBuilder2)
        Glide.with(this).asBitmap()
                .load("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1602579996309&di=15976afa6b07e6f54343351255fa2432&imgtype=0&src=http%3A%2F%2Ft9.baidu.com%2Fit%2Fu%3D1307125826%2C3433407105%26fm%3D79%26app%3D86%26f%3DJPEG%3Fw%3D5760%26h%3D3240")
                .thumbnail(requestBuilder1)
                .apply(RequestOptions.placeholderOf(R.drawable.thumbnail_bg))
                .into(iv)

在初次(记得排除缓存策略的干扰哦)运行的过程中,iv控件的图片加载顺序为:R.drawable.thumbnail_bg->requestBuilder2->requestBuilder1->最终图。

总结

文章中我们分析了在完全初始化状态之后,首次运行时,Glide框架组织Request责任链的全流程。总结重点如下:

  1. RequestBuilder通过尾递归的方式建立Request执行链。
  2. 在启动过程中,Request链通过首递归的顺序启动,并通过持有上一层节点的方式进行短路判断。

最后,下一篇文章,我们一定会分析Glide中的又一个重量级核心类:Engine,和它所用到的所有缓存策略。