kotlin-retrofit网络请求中的一些问题

1,479 阅读7分钟

这一篇其实是kotlin-协程与网络请求结果回调浅析的延续博客。

进入项目组的时候网络请求组件已经搭建好了,后来就一直按项目中的标准写法去使用,自己并没有深入的去解开心中的一些疑问,这篇博客主要是记录解开疑问的过程,以问答的形式展开。

一、retrofit是如何支持协程suspend的?

先看一个最普通的挂起函数编译成Java的结果

//编译前
suspend fun test(){}

//编译后
@Nullable
public final Object test(@NotNull Continuation $completion) {
   return Unit.INSTANCE;
}

会多一个Continuation参数,retrofit正是检测这个参数来判断是否为挂起函数的,源码如下:

//retrofit2包--->RequestFactory类-->parseParameter函数

//源码
private @Nullable ParameterHandler<?> parseParameter(
    int p, Type parameterType, @Nullable Annotation[] annotations, boolean allowContinuation) {
  ParameterHandler<?> result = null;
  //...略...
  if (result == null) {
    if (allowContinuation) {    //允许使用协程
      try {
        if (Utils.getRawType(parameterType) == Continuation类.class) {   //判断是不是Continuation类
          isKotlinSuspendFunction = true;    //判定为kotlin的协程挂起函数
          return null;
        }
      } catch (NoClassDefFoundError ignored) {
      }
    }
    throw parameterError(method, p, "No Retrofit annotation found.");
  }

  return result;
}

判定isKotlinSuspendFunctiontrue,在HttpServiceMethod类的parseAnnotations()函数就会执行下面的方法:

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(){
    //...略...
    if (!isKotlinSuspendFunction) {   //非Kotlin协程挂起函数
      return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
    } else if (continuationWantsResponse) {    //协程挂起函数
      return (HttpServiceMethod<ResponseT, ReturnT>)
          new SuspendForResponse<>(    //返回SuspendForResponse对象
              requestFactory,
              callFactory,
              responseConverter,
              (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
    } else {
      return (HttpServiceMethod<ResponseT, ReturnT>)
          new SuspendForBody<>(        //或返回SuspendForBody对象
              requestFactory,
              callFactory,
              responseConverter,
              (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
              continuationBodyNullable);
    }
}

SuspendForResponseSuspendForBody的源码中就有真正发起网络请求的地方

static final class SuspendForResponse<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
  private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;

  SuspendForResponse(
      RequestFactory requestFactory,
      okhttp3.Call.Factory callFactory,
      Converter<ResponseBody, ResponseT> responseConverter,
      CallAdapter<ResponseT, Call<ResponseT>> callAdapter) {
    super(requestFactory, callFactory, responseConverter);
    this.callAdapter = callAdapter;
  }

  @Override
  protected Object adapt(Call<ResponseT> call, Object[] args) {
    call = callAdapter.adapt(call);

    //noinspection unchecked Checked by reflection inside RequestFactory.
    Continuation<Response<ResponseT>> continuation =
        (Continuation<Response<ResponseT>>) args[args.length - 1];

    // See SuspendForBody for explanation about this try/catch.
    try {
      return KotlinExtensions.awaitResponse(call, continuation);      //发起网络请求的地方,执行call
    } catch (Exception e) {
      return KotlinExtensions.suspendAndThrow(e, continuation);
    }
  }
}

static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
  private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
  private final boolean isNullable;

  SuspendForBody(
      RequestFactory requestFactory,
      okhttp3.Call.Factory callFactory,
      Converter<ResponseBody, ResponseT> responseConverter,
      CallAdapter<ResponseT, Call<ResponseT>> callAdapter,
      boolean isNullable) {
    super(requestFactory, callFactory, responseConverter);
    this.callAdapter = callAdapter;
    this.isNullable = isNullable;
  }

  @Override
  protected Object adapt(Call<ResponseT> call, Object[] args) {
    call = callAdapter.adapt(call);

    Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];

    try {
      return isNullable
          ? KotlinExtensions.awaitNullable(call, continuation)      //发起网络请求的地方,执行call
          : KotlinExtensions.await(call, continuation);             //发起网络请求的地方,执行call
    } catch (Exception e) {
      return KotlinExtensions.suspendAndThrow(e, continuation);
    }
  }
}

二、retrofit发起的网络请求是如何取消的?

以上面KotlinExtensions.await(call, continuation)函数的源码为例,suspendCancellableCoroutine在博客的第一篇已经结束过,不熟悉的可以回头去看:

suspend fun <T : Any> Call<T>.await(): T {
  return suspendCancellableCoroutine { continuation ->   //可取消的协程作用域,保持挂起状态避免协程退出,需要手动恢复协程的挂起
    continuation.invokeOnCancellation {    //协程作用域被取消时执行cancel()方法,也就是网络请求在协程作用域被取消时取消网络请求的地方
      cancel()
    }
    enqueue(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
        if (response.isSuccessful) {
          val body = response.body()
          if (body == null) {
            val invocation = call.request().tag(Invocation::class.java)!!
            val method = invocation.method()
            val e = KotlinNullPointerException("Response from " +
                method.declaringClass.name +
                '.' +
                method.name +
                " was null but response body type was declared as non-null")
            continuation.resumeWithException(e)    //网络请求执行有结果恢复协程并回调结果
          } else {
            continuation.resume(body)    //网络请求执行有结果恢复协程并回调结果
          }
        } else {
          continuation.resumeWithException(HttpException(response))    //网络请求执行有结果恢复协程并回调结果
        }
      }

      override fun onFailure(call: Call<T>, t: Throwable) {
        continuation.resumeWithException(t)   //网络请求执行有结果恢复协程并回调结果
      }
    })
  }
}

所以答案就是当外部协程作用域被cancel的时候,网络请求也会被取消掉。

三、CoroutineCallAdapterFactory插件是如何取消网络请求的?

CoroutineCallAdapterFactory插件是早期retrofit还不支持suspend的时候JakeWharton大神推出的一个插件,github地址为CoroutineCallAdapterFactory。现在已经用不上了。

使用方法是:

①给retrofit添加插件

override fun setRetrofitBuilder(builder: Retrofit.Builder): Retrofit.Builder {
    return builder.apply {
        addConverterFactory(GsonConverterFactory.create(GsonFactory.getSingletonGson()))
        addCallAdapterFactory(CoroutineCallAdapterFactory())   //这里
    }
}

②需要返回Deferred对象

@GET("api/user/profile/userInfo")
fun getUserInfo(): Deferred<ApiResponse<User>>    //不能添加suspend关键字,返回值需要是Deferred

有了上面的基础看一下CoroutineCallAdapterFactory的源码,取其中一段,CompletableDeferred第一篇也讲过:

private class BodyCallAdapter<T>(
    private val responseType: Type
) : CallAdapter<T, Deferred<T>> {

    override fun responseType() = responseType

    override fun adapt(call: Call<T>): Deferred<T> {
        val deferred = CompletableDeferred<T>()    //使用CompletableDeferred

        deferred.invokeOnCompletion {
            if (deferred.isCancelled) {
                call.cancel()              //协程取消时取消网络请求
            }
        }

        call.enqueue(object : Callback<T> {
            override fun onFailure(call: Call<T>, t: Throwable) {
                deferred.completeExceptionally(t)    //网络请求的结果回调
            }

            override fun onResponse(call: Call<T>, response: Response<T>) {
                if (response.isSuccessful) {
                    deferred.complete(response.body()!!)    //网络请求的结果回调
                } else {
                    deferred.completeExceptionally(HttpException(response))    //网络请求的结果回调
                }
            }
        })

        return deferred
    }
}

当协程被取消时,网络请求也会被取消。

四、retrofit封装网络请求时是否要把请求体放异步协程,把回调放主协程?

下面是一个抽取的部分示例代码:

fun <T> BaseViewModel.request(
    block: suspend () -> BaseResponse<T>,
    success: (T) -> Unit,
    error: (AppException) -> Unit = {}
): Job {
    return viewModelScope.launch {   //1
        runCatching {
            //请求体
            block()
        }.onSuccess {
            success(t)
        }.onFailure {
            //打印错误消息
            it.message?.loge
            //失败回调
            error(ExceptionHandle.handleException(it))
        }
    }
}

使用successerror高阶函数回调结果,再后续使用LiveData回调到UI线程处理结果。

代码1处给人的错觉就是请求体似乎在主线程执行网络请求,但当看日志的时候又是在异步线程,原因在哪里呢? 其实答案已经在上面的源码中:

suspend fun <T : Any> Call<T>.await(): T {
  return suspendCancellableCoroutine { continuation ->  
    continuation.invokeOnCancellation {    
      cancel()
    }
    enqueue(object : Callback<T> {    //答案就在这里enqueue,retrofit主动帮我们放在了异步线程执行
      override fun onResponse(call: Call<T>, response: Response<T>) {
        //......
      }

      override fun onFailure(call: Call<T>, t: Throwable) {
        //......
      }
    })
  }
}

原来是retrofit主动帮我们放在了异步线程执行,所以1处不需要使用viewModelScope.launch(Dispatchers.IO) {...},直接使用Dispatchers.Main(默认)即可。

五、网络请求在ViewModel中,需要在页面退出时(onDestroy)时手动取消viewModeScope的协程作用域来取消网络请求吗?

ViewModel的生命周期其实跟Activity是同步的,ViewModel是从ActivityViewModelStore中取的,在这里它们也就形成了绑定关系,当Activity销毁时,ViewModel也会被销毁,这一点源码中有体现:

getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
            @NonNull Lifecycle.Event event) {
        if (event == Lifecycle.Event.ON_DESTROY) {
            // Clear out the available context
            mContextAwareHelper.clearAvailableContext();
            // And clear the ViewModelStore
            if (!isChangingConfigurations()) {
                getViewModelStore().clear();   //移除与Activity绑定的ViewModel
            }
        }
    }

当ViewModel被销毁时,会回调onClear方法,而onClear方法执行时会cancelViewModelScope中的协程:

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"

/**   注释中有说明:如果ViewModel被cleared,协程作用域将会被cancel。
 * [CoroutineScope] tied to this [ViewModel].
 * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
public val ViewModel.viewModelScope: CoroutineScope    //viewModelScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}

所以,如果使用ViewModelScope协程作用域发起的网络请求,当页面销毁时,ViewModelclearedViewModelScopecancel,网络请求就也同时取消了,不需要手动再维护了。

六、协程取消了,网络请求就一定取消了吗?

不一定,网络请求是异步线程执行的,所以有时候即使协程取消,网络请求也不一定取消。但是因为网络请求的回调是在ViewModelScope中执行的,协程作用域被cancel,自然一般情况下这里也就没有结果回调了。先借助下面的代码理解,后面会指出代码中的问题:

fun <T> BaseViewModel.request(
    block: suspend () -> BaseResponse<T>,
    success: (T) -> Unit,
    error: (AppException) -> Unit = {}
): Job {
    return viewModelScope.launch {   
        runCatching {
            //请求体
            block()
        }.onSuccess {
            success(t)    //结果回调1
        }.onFailure {
            //打印错误消息
            it.message?.loge
            //失败回调
            error(ExceptionHandle.handleException(it))    //结果回调2
        }
    }
}

七、协程被取消时会在onFailure中回调job was cancled,如何防止回调到主线程刷新UI?

在问题六的代码中,如果粗心大意在结果回调2处直接使用LiveData回调到主线程并对UI进行操作,就容易出现问题,比如协程取消时,onFailure会回调下面这个异常:

private fun Throwable?.orCancellation(job: Job): Throwable = this ?: JobCancellationException("Job was cancelled", null, job)

这个时候可能是因为页面关闭导致viewModelScopecancel回调过来的结果,页面都关闭了还操作UI肯定会出现问题。那如何改进问题六中的代码呢? 下面是我知道的二种方式:

方式一:回调之前添加对viewModelScope的判断:

if (viewModelScope.isActive) {    //判断viewModelScope是否为激活状态
    //失败回调
    error(ExceptionHandle.handleException(it))
}

方式二:在Activity或fargment中用lifecycleScope包一层:

lifecycleScope.launch {
    mViewModel.userInfoResult.observe(this@StartActivity) {
        mViewBind.bt.visibility = View.GONE
    } 
}

因为页面销毁时,lifecycleScope协程作用域也会被销毁,里面的代码也就不会被执行。

参考了以下内容

Retrofit解密:接口请求是如何适配suspend协程?

如何让 Android 网络请求像诗一样优雅

博客主要是记录和总结,如有错误欢迎批评指正!