Glide 源码阅读笔记(三)

Glide 源码阅读笔记(三)

在第一篇文章中我简单介绍了 Glide 实例的创建过程,重点介绍了 Glide 的内存缓存实现和磁盘的缓存实现:Glide 源码阅读笔记(一)

在第二篇文章中介绍了 Glide 对生命周期的管理: Glide 源码阅读笔记(二)

今天我们继续今天的内容。

源码阅读基于 Glide 4.16.0 版本

Reqeust 和 RequestCoordinator

Request 比较好理解,它就是一个图片加载的任务。RequestCoordinator 它也是继承于 Request,它是用来协调它内部的多个 Request,这么说比较抽象。我这里简单举一个例子。我们可以在使用 Glide 时,添加一个异常时请求的任务,当主任务加载失败时 Glide 就会去加载这个异常时请求的任务。例如以下代码:

Kotlin 复制代码
val errorRequestBuilder = Glide.with(this)
    .load("https://www.tans.com/error.png")
Glide.with(this)
    .load("https://upload-images.jianshu.io/upload_images/5809200-a99419bb94924e6d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240")
    .error(errorRequestBuilder)
    .into(findViewById<ImageView>(R.id.my_iv))

其中如何协调主 Request 与错误相关的 Request,就是 ErrorRequestCoordinator 的工作,它的工作原理非常简单,首先触发主 Request,当它请求失败时再去加载异常的 Request。类似原理的还有 ThumbnailRequestCoordinator,它是协调缩略图加载的 RequestCoordinator。后面的代码分析中我们都能看到他们。通过 RequestCoordinator 可以把单个 Request 构建成一个 Request 的请求树,可以用这个请求树来执行复杂的请求逻辑。

我们在使用 Glide 构建完 RequestBuilder 后,会通过 into() 方法把最后的结果渲染到对应 ImageView (当然我们也可以不渲染到 ImageView 中,这需要我们自定义)。所以我们也以 RequestBuilder#into() 方法作为今天分析的入口函数:

Java 复制代码
  @NonNull
  public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) {
    // 校验必须在主线程
    Util.assertMainThread();
    Preconditions.checkNotNull(view);

    BaseRequestOptions<?> requestOptions = this;
    // 如果没有设置图片的裁剪方式,根据 ImageView 的 ScaleType 来计算。
    if (!requestOptions.isTransformationSet()
        && requestOptions.isTransformationAllowed()
        && view.getScaleType() != null) {
      // Clone in this method so that if we use this RequestBuilder to load into a View and then
      // into a different target, we don't retain the transformation applied based on the previous
      // View's scale type.
      // 计算裁剪方式
      switch (view.getScaleType()) {
        case CENTER_CROP:
          requestOptions = requestOptions.clone().optionalCenterCrop();
          break;
        case CENTER_INSIDE:
          requestOptions = requestOptions.clone().optionalCenterInside();
          break;
        case FIT_CENTER:
        case FIT_START:
        case FIT_END:
          requestOptions = requestOptions.clone().optionalFitCenter();
          break;
        case FIT_XY:
          requestOptions = requestOptions.clone().optionalCenterInside();
          break;
        case CENTER:
        case MATRIX:
        default:
          // Do nothing.
      }
    }

    return into(
        // 将 ImageView 封装成 Target
        glideContext.buildImageViewTarget(view, transcodeClass),
        /* targetListener= */ null,
        requestOptions,
        // 回调线程设置成主线程
        Executors.mainThreadExecutor());
  }

简单说明下上面的代码:

  1. into() 方法必须在主线程调用。
  2. 如果 RequestBuilder 没有设置裁剪方式,需要通过 ImageView#getScaleType() 来计算,处理裁剪方式的类是 Transformations,它被保存在 options 中,后面我们会再看到它的出现。
  3. 然后通过 GlideContext#buildImageViewTarget()ImageView 封装成 ViewTarget 对象,这里要注意我们的 trascodeClassDrawable,它表示 ViewTarget 最后能够处理渲染的对象格式,然后继续调用 into() 方法。

我们去看看 GlideContext#buildImageViewTarget() 方法是如何构建 ViewTarget 的:

Java 复制代码
  @NonNull
  public <X> ViewTarget<ImageView, X> buildImageViewTarget(
      @NonNull ImageView imageView, @NonNull Class<X> transcodeClass) {
    return imageViewTargetFactory.buildTarget(imageView, transcodeClass);
  }

继续调用 ImageViewTargetFactory#buildTarget() 方法:

Java 复制代码
  @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)");
    }
  }

它所支持的 transcodeClass 只能是 Bitmap 或者 Drawable,我们的 demo 中是 Drawable,最终的 ViewTarget 实现类是 DrawableImageViewTarget()

在看完了 ViewTarget 创建后,我们继续我们的主流程,继续看 into() 方法:

Java 复制代码
  private <Y extends Target<TranscodeType>> Y into(
      @NonNull Y target,
      @Nullable RequestListener<TranscodeType> targetListener,
      BaseRequestOptions<?> options,
      Executor callbackExecutor) {
    Preconditions.checkNotNull(target);
    // 如果没有这是 model,直接报错。
    if (!isModelSet) {
      throw new IllegalArgumentException("You must call #load() before calling #into()");
    }
    // 构建 Request
    Request request = buildRequest(target, targetListener, options, callbackExecutor);
    
    // 获取上次的 Request
    Request previous = target.getRequest();
    // 如果上次的 Request 和 当前的请求一致,直接开始上次的请求
    if (request.isEquivalentTo(previous)
        && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
      if (!Preconditions.checkNotNull(previous).isRunning()) {
        previous.begin();
      }
      return target;
    }
    
    // 清除 Target 中上次的请求。
    requestManager.clear(target);
    // 将当前的 Request 设置到 Target 中。
    target.setRequest(request);
    // 将 target 和 request 通知 RequestManager
    requestManager.track(target, request);

    return target;
  }

简单说明下上面的代码:

  1. 检查是否有设置 model,如果没有就直接报错,我们设置的请求 url 就是 model
  2. 通过 buildRequest() 构建 Request(后面重点分析这个方法)。
  3. 判断 ViewTarget 中上次的 Request 和当前的 Request 是否一致,如果一致,就直接返回当前方法(返回之前会判断一下上次的 Request 的运行状态,如果没有运行调用对应的 begin() 方法)。
  4. 通过 RequestManager#clear() 方法清除 Target 中的 Request
  5. 通过 Target#setRequest() 方法将当前 Request 添加到 Target 中。
  6. TargetRequest 通知 RequestManager

我们简单看看 ViewTarget#setRequest() 方法是如何设置 Request 的:

Java 复制代码
  @Override
  public void setRequest(@Nullable Request request) {
    setTag(request);
  }
  
  private void setTag(@Nullable Object tag) {
    isTagUsedAtLeastOnce = true;
    view.setTag(tagId, tag);
  }

Request 其实就是被添加到 Viewtag 上,对应的 tagIdR.id.glide_custom_view_target_tag

我们再看看 RequestManager#track() 方法是如何处理 TargetRequest 的:

Java 复制代码
  synchronized void track(@NonNull Target<?> target, @NonNull Request request) {
    targetTracker.track(target);
    requestTracker.runRequest(request);
  }

这里 target 交由 TargetTracker 处理;request 交由 RequestTracker 来处理。

Java 复制代码
public void track(@NonNull Target<?> target) {  
   targets.add(target);  
}

TargetTracker 处理非常简单,直接添加到列表中。

再看看 RequestTracker#runRequest() 方法:

Java 复制代码
  public void runRequest(@NonNull Request request) {
    requests.add(request);
    if (!isPaused) {
      request.begin();
    } else {
      request.clear();
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Paused, delaying request");
      }
      pendingRequests.add(request);
    }
  }

request 添加到 requests 列表中,如果当前没有暂停(也就是 onStart() 生命周期)调用 Request#begin() 方法;如果当前已经被暂停,会调用 Request#clear() 方法,然后添加到 pendingRequests 列表中。

我们继续看看 RequestBuilder#into() 方法中调用过的 buildeRequest() 方法(希望你还没有忘记),看看它是如何构建 Request 的。

Java 复制代码
  private Request buildRequest(
      Target<TranscodeType> target,
      @Nullable RequestListener<TranscodeType> targetListener,
      BaseRequestOptions<?> requestOptions,
      Executor callbackExecutor) {
    return buildRequestRecursive(
        /* requestLock= */ new Object(),
        target,
        targetListener,
        /* parentCoordinator= */ null,
        transitionOptions,
        requestOptions.getPriority(),
        requestOptions.getOverrideWidth(),
        requestOptions.getOverrideHeight(),
        requestOptions,
        callbackExecutor);
  }

继续看 buildRequestRecursive() 方法的实现:

Java 复制代码
  private Request buildRequestRecursive(
      Object requestLock,
      Target<TranscodeType> target,
      @Nullable RequestListener<TranscodeType> targetListener,
      @Nullable RequestCoordinator parentCoordinator,
      TransitionOptions<?, ? super TranscodeType> transitionOptions,
      Priority priority,
      int overrideWidth,
      int overrideHeight,
      BaseRequestOptions<?> requestOptions,
      Executor callbackExecutor) {

    // Build the ErrorRequestCoordinator first if necessary so we can update parentCoordinator.
    ErrorRequestCoordinator errorRequestCoordinator = null;
    // 如果有 ErrorBuilder,构建一个 ErrorRequestCoordinator,同时它作为 parentCoordinator
    if (errorBuilder != null) {
      errorRequestCoordinator = new ErrorRequestCoordinator(requestLock, parentCoordinator);
      parentCoordinator = errorRequestCoordinator;
    }
    
    // 构建 MainRequest
    Request mainRequest =
        buildThumbnailRequestRecursive(
            requestLock,
            target,
            targetListener,
            parentCoordinator,
            transitionOptions,
            priority,
            overrideWidth,
            overrideHeight,
            requestOptions,
            callbackExecutor);

    if (errorRequestCoordinator == null) {
      // 如果没有对 Error 的处理,直接返回 mainRequest
      return mainRequest;
    }

    int errorOverrideWidth = errorBuilder.getOverrideWidth();
    int errorOverrideHeight = errorBuilder.getOverrideHeight();
    if (Util.isValidDimensions(overrideWidth, overrideHeight) && !errorBuilder.isValidOverride()) {
      errorOverrideWidth = requestOptions.getOverrideWidth();
      errorOverrideHeight = requestOptions.getOverrideHeight();
    }
    
    // 通过 errorBuilder 构建 errorRequest。
    Request errorRequest =
        errorBuilder.buildRequestRecursive(
            requestLock,
            target,
            targetListener,
            errorRequestCoordinator,
            errorBuilder.transitionOptions,
            errorBuilder.getPriority(),
            errorOverrideWidth,
            errorOverrideHeight,
            errorBuilder,
            callbackExecutor);
    // 将 mainRequest 和 errorRequest 添加到 ErrorRequestCoordinator 中        
    errorRequestCoordinator.setRequests(mainRequest, errorRequest);
    return errorRequestCoordinator;
  }

简单说明下上面的方法:

  1. 如果有 errorBuilder,构建一个 ErrorRequestCoordinator,同时它也是后续请求的 parent
  2. 通过 buildThumbnailRequestRecursive() 构建 mainRequest。如果没有 errorBuilder,直接把 mainRequest 作为结果返回方法。
  3. 通过 errorBuilder 构建 errorRequest
  4. mainRequesterrorRequest 添加到 ErrorRequestCoordinator 中,然后把它作为结果返回。

我们继续看看 buildThumbnailRequestRecursive() 方法是如何 mainRequest 的。

Java 复制代码
  private Request buildThumbnailRequestRecursive(
      Object requestLock,
      Target<TranscodeType> target,
      RequestListener<TranscodeType> targetListener,
      @Nullable RequestCoordinator parentCoordinator,
      TransitionOptions<?, ? super TranscodeType> transitionOptions,
      Priority priority,
      int overrideWidth,
      int overrideHeight,
      BaseRequestOptions<?> requestOptions,
      Executor callbackExecutor) {
    // 判断是否有 thumbnailBuilder  
    if (thumbnailBuilder != null) {
      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;

      if (thumbnailBuilder.isDefaultTransitionOptionsSet) {
        thumbTransitionOptions = transitionOptions;
      }

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

      int thumbOverrideWidth = thumbnailBuilder.getOverrideWidth();
      int thumbOverrideHeight = thumbnailBuilder.getOverrideHeight();
      if (Util.isValidDimensions(overrideWidth, overrideHeight)
          && !thumbnailBuilder.isValidOverride()) {
        thumbOverrideWidth = requestOptions.getOverrideWidth();
        thumbOverrideHeight = requestOptions.getOverrideHeight();
      }
      
      // 创建一个 ThumbnailRequestCoordinator 对象
      ThumbnailRequestCoordinator coordinator =
          new ThumbnailRequestCoordinator(requestLock, parentCoordinator);
      // 构建 fullRequest,这其实就是我们的主请求    
      Request fullRequest =
          obtainRequest(
              requestLock,
              target,
              targetListener,
              requestOptions,
              coordinator,
              transitionOptions,
              priority,
              overrideWidth,
              overrideHeight,
              callbackExecutor);
      isThumbnailBuilt = true;
      // 构建 thumbRequest。
      Request thumbRequest =
          thumbnailBuilder.buildRequestRecursive(
              requestLock,
              target,
              targetListener,
              coordinator,
              thumbTransitionOptions,
              thumbPriority,
              thumbOverrideWidth,
              thumbOverrideHeight,
              thumbnailBuilder,
              callbackExecutor);
      isThumbnailBuilt = false;
      // 将 fullRequest 和 thumbRequest 添加到 ThumbnailRequestCoordinator 中
      coordinator.setRequests(fullRequest, thumbRequest);
      return coordinator;
    } else if (thumbSizeMultiplier != null) {
      // ...
      return coordinator;
    } else {
      // 没有 thumbnailBuilder,直接构建一个 SingleRequest
      return obtainRequest(
          requestLock,
          target,
          targetListener,
          requestOptions,
          parentCoordinator,
          transitionOptions,
          priority,
          overrideWidth,
          overrideHeight,
          callbackExecutor);
    }
  }
  
  private Request obtainRequest(
      Object requestLock,
      Target<TranscodeType> target,
      RequestListener<TranscodeType> targetListener,
      BaseRequestOptions<?> requestOptions,
      RequestCoordinator requestCoordinator,
      TransitionOptions<?, ? super TranscodeType> transitionOptions,
      Priority priority,
      int overrideWidth,
      int overrideHeight,
      Executor callbackExecutor) {
    return SingleRequest.obtain(
        context,
        glideContext,
        requestLock,
        model,
        transcodeClass,
        requestOptions,
        overrideWidth,
        overrideHeight,
        priority,
        target,
        targetListener,
        requestListeners,
        requestCoordinator,
        glideContext.getEngine(),
        transitionOptions.getTransitionFactory(),
        callbackExecutor);
  }

上面的代码看似很多,其实非常简单:

  1. 判断是否有 thumbnailBuilder (也就是缩略图),如果没有,直接构建一个 SingleRequest 返回,真实的请求实现都是 SingleRequest 兑现。
  2. 构建一个 ThumbnailRequestCoordinator 对象,它会作为后续请求的 parent
  3. 构建主请求 fullRequest 和缩略图请求 thumbRequest,并把他们添加到 ThumbnailRequestCoordinator 中,然后将它作为结果返回。

所以到这里我们可以做一个请求构建的总结了,我们以最复杂的情况为例子来说明,也就是同时有 ErrorRequestThumbnailReqeust 的请求:

首先构建一个 ErrorRequestCoordinator,然后通过 errorRequestBuilder 构建一个 errorRequest,继续构建一个 thumbnailRequest。他们都会被添加到 ErrorRequestCoordinator 中,他们的 parent 也都是 ErrorRequestCoordinator

构建 thumnailRequest 时,先创建一个 ThumbnailRequestCoordinator,然后构建一个 fullRequest,也就是主请求,然后构建一个 thumbnailRequest,也就是缩略图请求。他们也都被添加到 ThumbnailRequestCoordinator 中,parent 也都是它。

ErrorRequestCoordinator

我们先简单看看 ErrorRequestCoordinator#begin() 方法的实现:

Java 复制代码
  @Override
  public void begin() {
    synchronized (requestLock) {
      if (primaryState != RequestState.RUNNING) {
        primaryState = RequestState.RUNNING;
        primary.begin();
      }
    }
  }

直接触发主请求的 Request#begin()

我们继续看看 ErrorRequestCoordinator#onRequestFailed() 方法中是如何处理失败请求的:

Java 复制代码
  @Override
  public void onRequestFailed(Request request) {
    synchronized (requestLock) {
      // 如果当前失败的请求不是 errorRequest,也就是表示主请求失败了
      if (!request.equals(error)) {
        // 触发 error#begin() 方法
        primaryState = RequestState.FAILED;
        if (errorState != RequestState.RUNNING) {
          errorState = RequestState.RUNNING;
          error.begin();
        }
        return;
      }
      
      // 以下的情况表示主请求和失败请求都失败了
      errorState = RequestState.FAILED;

      if (parent != null) {
        // 通知 parent 请求失败了
        parent.onRequestFailed(this);
      }
    }
  }

如果主请求失败,就会触发 error 请求开始;如果主请求和错误的请求都失败,就表示当前请求失败了,然后它会通知 parent

ThumbnailRequestCoordinator

Java 复制代码
  @Override
  public void begin() {
    synchronized (requestLock) {
      isRunningDuringBegin = true;
      try {
        // 如果 full 请求没有完成,同时 thumb 也没有在请求中,触发 thumb 请求。
        if (fullState != RequestState.SUCCESS && thumbState != RequestState.RUNNING) {
          thumbState = RequestState.RUNNING;
          thumb.begin();
        }
        // 如果 full 没有在请求中,触发 full 的请求。
        if (isRunningDuringBegin && fullState != RequestState.RUNNING) {
          fullState = RequestState.RUNNING;
          full.begin();
        }
      } finally {
        isRunningDuringBegin = false;
      }
    }
  }

如果 full 请求没有完成,同时 thumb 也没有在请求中,触发 thumb 请求;如果 full 没有在请求中,触发 full 的请求。也就是它会同时触发 fullthumb 的请求。

我们继续看看 onRequestSuccess() 方法是如何处理请求成功的任务的:

Java 复制代码
  @Override
  public void onRequestSuccess(Request request) {
    synchronized (requestLock) {
      if (request.equals(thumb)) {
        thumbState = RequestState.SUCCESS;
        return;
      }
      fullState = RequestState.SUCCESS;
      if (parent != null) {
        parent.onRequestSuccess(this);
      }
      if (!thumbState.isComplete()) {
        thumb.clear();
      }
    }
  }

如果是 thumb 请求成功,只是简单更新状态,然后返回;如果是 full 请求成功,会通知 parent,然后把 thumb 的请求取消掉。

我们再看看它是如何处理请求失败的任务的:

Java 复制代码
  @Override
  public void onRequestFailed(Request request) {
    synchronized (requestLock) {
      if (!request.equals(full)) {
        thumbState = RequestState.FAILED;
        return;
      }
      fullState = RequestState.FAILED;

      if (parent != null) {
        parent.onRequestFailed(this);
      }
    }
  }

也是朴实无华的代码,当 thumb 请求失败了,也是简单更新状态;full 请求失败了,更新 状态,同时通知 parent

简单介绍 Registry

Registry 中注册了许多的重要的功能组件,它是通过 RegistryFactory 来创建实例和初始化的:

Java 复制代码
  @Synthetic
  static Registry createAndInitRegistry(
      Glide glide,
      List<GlideModule> manifestModules,
      @Nullable AppGlideModule annotationGeneratedModule) {

    BitmapPool bitmapPool = glide.getBitmapPool();
    ArrayPool arrayPool = glide.getArrayPool();
    Context context = glide.getGlideContext().getApplicationContext();

    GlideExperiments experiments = glide.getGlideContext().getExperiments();

    Registry registry = new Registry();
    // 注册默认的系统组件
    initializeDefaults(context, registry, bitmapPool, arrayPool, experiments);
    // 注册自定义的组件,或者替换默认的系统组件
    initializeModules(context, glide, registry, manifestModules, annotationGeneratedModule);
    return registry;
  }

通过 initializeDefaults() 来注册系统组件,等我们后续需要时再来查看这个方法;通过 initializeModules() 来添加默认的组件,我们先来看看它的实现:

Java 复制代码
  private static void initializeModules(
      Context context,
      Glide glide,
      Registry registry,
      List<GlideModule> manifestModules,
      @Nullable AppGlideModule annotationGeneratedModule) {
    for (GlideModule module : manifestModules) {
      try {
        module.registerComponents(context, glide, registry);
      } catch (AbstractMethodError e) {
        throw new IllegalStateException(
            "Attempting to register a Glide v3 module. If you see this, you or one of your"
                + " dependencies may be including Glide v3 even though you're using Glide v4."
                + " You'll need to find and remove (or update) the offending dependency."
                + " The v3 module name is: "
                + module.getClass().getName(),
            e);
      }
    }
    if (annotationGeneratedModule != null) {
      annotationGeneratedModule.registerComponents(context, glide, registry);
    }
  }

不知道你是否还记得我们是怎么把原来的网络请求替换成 OkHttp 的?就是要通过上面方法触发我们自定义的 AppGlideModuleregisterComponents() 方法,通过这个方法我们就可以替换原有的网络请求为 OkHttp

看到这里你可能还是不太清楚 Registry 中具体注册了什么样功能的组件。我这里以一次简单的网络请求任务来描述一下其中会用到哪些组件:

首先我们输入的 url 是一个 String 对象,在 Glide 内部中被称为 model (它也可以是 File 类型,Drawable 类型等等),首先就需要 ModelLoader 来转换处理 model。所以 ModelLoader 就有一个输入类型和输出类型,首先要找到一个 ModelLoaderString 类型的 model 转换成 Url;然后继续找到一个 ModelLoaderUrl 类型的 model 转换成 GlideUrl;最后找到将 GlideUrl 转换成 InputStream 类型的 ModelLoader,没错这个 ModelLoader 其实也就是执行的网络请求,它也是处理的最后一个 ModelLoader,最后的处理结果就需要交由下一站的组件来处理。

这也是为什么替换原有的网络请求为 OkHttp,的代码是下面这样:

Kotlin 复制代码
    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        super.registerComponents(context, glide, registry)
        registry.replace(
            GlideUrl::class.java,
            InputStream::class.java,
            OkHttpUrlLoader.Factory(appOkHttpClient)
        )
    }

ModelLoader 处理的下一站就是 Decoder,我们的 ModelLoader 的输出类型是 InputStream,我们就需要找到一个 Decoder 支持的输入类型是 InputStream的,把 InputStream 解码成 Bitmap

在到下一站就是 Transcoder,同样的它也是有一个输入类型和输出类型,Decoder 传过来的类型是 Bitmap,我们需要将它处理成 ViewTarget 需要的 Drawable 类型,然后 ImageView 就能够直接渲染 Drawable 了。

我只是简单介绍了 Registry 中的 ModelLoaderDecoderTranscoder 组件,他们是非常核心的组件。其中还有一些别的组件,比如在缓存到文件时就需要将 Bitmap 重新编码,这时就需要 Encoder

最后

本篇文件介绍了 ReqeustRequestCoordinator,还简单介绍了 Registry 中的核心组件(但是没有看源码)。由于本篇文章的篇幅也不短了,为了不像第一篇文章那样过长,所以后续的文章继续讲 SingleRequest 中是如何执行请求的。

相关推荐
大耳猫3 小时前
主动测量View的宽高
android·ui
帅次6 小时前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
枯骨成佛7 小时前
Android中Crash Debug技巧
android
kim565912 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼12 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ12 小时前
Android Studio使用c++编写
android·c++
csucoderlee13 小时前
Android Studio的新界面New UI,怎么切换回老界面
android·ui·android studio
kim565913 小时前
各版本android studio下载地址
android·ide·android studio
饮啦冰美式13 小时前
Android Studio 将项目打包成apk文件
android·ide·android studio
夜色。13 小时前
Unity6 + Android Studio 开发环境搭建【备忘】
android·unity·android studio