Kotlin 协程源码解析(八)Kotlin 协程源码解析:Retrofit 的 suspend 函数是如何恢复协程的?

0 阅读6分钟

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

它做的事情只有两件:

  1. 记录这个方法是 Kotlin suspend 函数。
  2. 不把 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 时讨论过的内容。