这一篇其实是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;
}
判定isKotlinSuspendFunction为true,在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);
}
}
SuspendForResponse和SuspendForBody的源码中就有真正发起网络请求的地方
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))
}
}
}
使用success和error高阶函数回调结果,再后续使用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是从Activity的ViewModelStore中取的,在这里它们也就形成了绑定关系,当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方法执行时会cancel掉ViewModelScope中的协程:
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协程作用域发起的网络请求,当页面销毁时,ViewModel被cleared,ViewModelScope被cancel,网络请求就也同时取消了,不需要手动再维护了。
六、协程取消了,网络请求就一定取消了吗?
不一定,网络请求是异步线程执行的,所以有时候即使协程取消,网络请求也不一定取消。但是因为网络请求的回调是在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)
这个时候可能是因为页面关闭导致viewModelScope被cancel回调过来的结果,页面都关闭了还操作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协程?
博客主要是记录和总结,如有错误欢迎批评指正!