Kotlin 协程源码解析(八)Kotlin 协程源码解析:Retrofit 的 suspend 函数是如何恢复协程的?
在之前的文章中,我们研究过 delay 是如何挂起协程,以及在时间到达之后恢复协程。
这一次我们来看一个更加贴近实际开发的例子:Retrofit 的 suspend 接口。
我们平时可能只是这样使用 Retrofit:
interface UserApi {
@GET("user")
suspend fun getUser(): User
}
调用时:
val user = api.getUser()
从使用者的角度来看,这就像一个普通的同步函数。
但实际上,网络请求显然是异步执行的。那么问题来了:
Retrofit 是如何让协程在网络请求期间挂起,又是如何在网络请求完成之后恢复这个协程的?
这篇文章就沿着 Retrofit 源码回答这个问题。
一、从 suspend 函数开始
首先我们需要知道,Retrofit 面对的并不是 Kotlin 源代码中的:
suspend fun getUser(): User
Kotlin 编译器会对 suspend 函数进行转换。
在 JVM 层面,可以粗略理解成:
Object getUser(Continuation<? super User> continuation);
也就是说,suspend 函数会多出一个 Continuation 参数。
这也是 Retrofit 判断一个接口方法是不是 suspend 函数的依据。
在 RequestFactory 中,Retrofit 会检查方法参数:
if (Utils.getRawType(parameterType) == Continuation.class) {
isKotlinSuspendFunction = true;
return null;
}
这里的 Continuation 并不是 HTTP 请求参数,因此 Retrofit 不会为它创建普通的 ParameterHandler。
它做的事情只有两件:
- 记录这个方法是 Kotlin
suspend函数。 - 不把
Continuation当成 HTTP 请求参数处理。
也就是说,RequestFactory 负责的只是识别:
这是一个 suspend 方法
真正的协程处理则发生在后面。
二、Retrofit 最终把请求交给 SuspendForBody
在 HttpServiceMethod.parseAnnotations() 中,Retrofit 根据前面得到的 isKotlinSuspendFunction 创建对应的 HttpServiceMethod。
对于普通的:
suspend fun getUser(): User
最终会使用:
new SuspendForBody<>(...)
而真正调用接口方法时,HttpServiceMethod.invoke() 会先创建一个 Call:
Call<ResponseT> call =
new OkHttpCall<>(
requestFactory,
instance,
args,
callFactory,
responseConverter
);
return adapt(call, args);
然后进入 SuspendForBody.adapt()。
源码中最关键的部分是:
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);
Continuation<ResponseT> continuation =
(Continuation<ResponseT>) args[args.length - 1];
...
return KotlinExtensions.await(call, continuation);
}
这里终于出现了我们一直在寻找的东西:
Continuation<ResponseT> continuation
这个 Continuation 就是 Kotlin 编译器为当前 suspend 调用提供的 Continuation。
Retrofit 接下来把它交给:
KotlinExtensions.await()
三、await() 是整个过程的关键
KotlinExtensions.await() 的实现非常简洁:
suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation { cancel() }
enqueue(
object : Callback<T> {
override fun onResponse(
call: Call<T>,
response: Response<T>
) {
if (response.isSuccessful) {
continuation.resume(response.body())
} else {
continuation.resumeWithException(
HttpException(response)
)
}
}
override fun onFailure(
call: Call<T>,
t: Throwable
) {
continuation.resumeWithException(t)
}
}
)
}
}
这里就是 Retrofit 实现协程挂起与恢复的核心。
我们可以把它拆成两个阶段:
suspendCancellableCoroutine
↓
挂起
↓
Call.enqueue()
↓
异步网络请求
↓
请求完成
↓
continuation.resume(...)
↓
恢复
四、suspendCancellableCoroutine:把协程交给 Retrofit
首先看:
suspendCancellableCoroutine { continuation ->
...
}
这里的 continuation 就是当前 await() 对应的 Continuation。
Retrofit 在这里做了一件非常重要的事情:
它把当前协程的 Continuation 保存到了异步网络请求的回调中。
接下来 Retrofit 并不需要阻塞当前线程等待网络请求。
它直接:
enqueue(...)
发起异步请求。
于是当前协程可以挂起。
可以把这个过程理解成:
当前协程
│
│ await()
↓
suspendCancellableCoroutine
│
├── 获得 Continuation
│
├── 注册网络请求回调
│
└── 挂起协程
│
│
↓
网络请求
此时网络请求还没有完成,但协程也不需要一直占着线程等待。
五、网络请求完成后,Retrofit 如何恢复协程?
这就是整篇文章最核心的部分。
Retrofit 调用了:
enqueue(
object : Callback<T> {
...
}
)
当 OkHttp 请求完成之后,就会进入 Retrofit 注册的 Callback。
如果请求成功:
override fun onResponse(
call: Call<T>,
response: Response<T>
) {
if (response.isSuccessful) {
continuation.resume(response.body())
}
}
这里:
continuation.resume(response.body())
就是恢复协程的关键代码。
网络请求得到:
User
然后 Retrofit 把这个结果交给:
Continuation<User>
通过:
resume(user)
通知协程:
异步操作已经完成,你可以继续执行了。
于是之前:
val user = api.getUser()
showUser(user)
被挂起的协程就可以继续向下执行。
六、失败时同样是恢复,只不过恢复的是异常
网络请求并不一定成功。
如果 HTTP 返回了错误状态码:
continuation.resumeWithException(
HttpException(response)
)
如果网络请求本身失败:
override fun onFailure(
call: Call<T>,
t: Throwable
) {
continuation.resumeWithException(t)
}
这里同样是在恢复 Continuation。
只不过:
continuation.resume(value)
表示:
异步操作成功,恢复并返回一个值。
而:
continuation.resumeWithException(exception)
表示:
异步操作失败,恢复协程并让它以异常结束。
因此:
try {
val user = api.getUser()
} catch (e: Exception) {
// ...
}
这里的异常最终也可以按照普通协程异常的方式被处理。
七、Retrofit 做的事情其实非常简单
到这里,我们已经可以回答最开始的问题了。
Retrofit 并没有自己实现一套协程调度机制。
它真正做的事情其实只有:
Retrofit
│
↓
suspendCancellableCoroutine
│
↓
获得 Continuation
│
↓
Call.enqueue()
│
↓
异步网络请求
│
┌─────────┴─────────┐
↓ ↓
onResponse() onFailure()
│ │
↓ ↓
continuation.resume() resumeWithException()
│ │
└─────────┬─────────┘
↓
协程恢复
所以 Retrofit 和 Kotlin 协程之间真正的连接点就是:
Continuation
Retrofit 不需要知道协程内部的状态机是如何工作的,也不需要自己负责把协程重新调度到某个线程。
它只需要在异步操作完成时告诉 Continuation:
resume(value)
或者:
resumeWithException(exception)
剩下的工作交给 Kotlin 协程机制完成。
八、这和我们之前研究的 delay 有什么不同?
我们之前研究 delay 时看到的是:
delay()
↓
协程挂起
↓
等待指定时间
↓
时间到达
↓
resume
↓
协程恢复
Retrofit 的流程其实非常相似:
await()
↓
协程挂起
↓
等待网络请求
↓
网络请求完成
↓
resume
↓
协程恢复
两者真正不同的地方,只在于:
什么事件决定了协程可以恢复。
delay 等待的是:
时间
Retrofit 等待的是:
网络请求完成
但从协程的角度来看,它们最终做的事情非常相似:
异步事件发生
↓
resume(Continuation)
↓
协程继续执行
这也是 Continuation 一个非常重要的意义:
它把“协程什么时候恢复”这件事情交给了外部异步系统。
这个外部系统可以是定时器,也可以是网络请求,甚至可以是其他任何异步 API。
九、最后总结
Retrofit 的 suspend 接口并没有什么神秘之处。
以:
@GET("user")
suspend fun getUser(): User
为例,核心流程可以概括为:
suspend fun getUser()
↓
Kotlin 编译器生成 Continuation 参数
↓
Retrofit 识别 Continuation
↓
KotlinExtensions.await()
↓
suspendCancellableCoroutine
↓
当前协程挂起
↓
Call.enqueue()
↓
OkHttp 异步执行网络请求
↓
请求完成
↓
Retrofit Callback
↓
continuation.resume(result)
↓
协程恢复
因此,Retrofit 实现 suspend 接口的核心并不是“让网络请求变成同步请求” 。
恰恰相反,它仍然使用异步的 Call.enqueue()。
真正发生的是:
异步网络请求
+
Continuation
↓
把网络请求的完成事件转换成协程的恢复事件
这就是 Retrofit 能够让我们用同步代码风格编写异步网络请求的原因。
而 Continuation.resume() 被调用之后,协程究竟是如何从这里继续执行,并最终回到它原来的 Dispatcher,这已经是 Kotlin 协程自身的恢复机制了,也是我们之前研究 delay 时讨论过的内容。