协程为 Kotlin 提供了更优雅的异步编程方式,但由于 Jvm 本身并没有提供协程支持,因此 Kotlin/Jvm 中的协程仿佛如魔法一般的存在。

这一次我们将对协程的实现原理进行解析。

挂起函数

函数声明转换

Kotlin 从语言层面提供了 suspend 关键字,用于表示该函数为挂起函数,但在 Jvm 中并没有可挂起的概念。

以下面的一个挂起函数声明为例:

suspend fun suspendingFunction(a: Int, vararg b: String)

如果通过 IDEA 将其编译的 Class 文件进行反编译,可以得到一个与其 Jvm 实现等价的 Java 方法声明:

public static final Object suspendingFunction(int a, @NotNull String[] b, @NotNull Continuation $completion)

挂起函数的 suspend 关键字转换成了一个额外的参数,其类型为 Continuation。这一操作是 Kotlin 编译器在编译期执行的,称为 CPS(Continuation-Passing Style,续体传递风格)转换,而在下一步分析协程实现原理之前,需要先对 “续体” 这一概念进行说明。

续体

采用回调的方法获取耗时操作的结果一般会类似如下例子中的形式:

request(arguments) { result ->
    runTask(result)
}

在上述例子中,Lambda 作为这一代码片段中 request() 函数执行完毕的后续操作,这一部分被称为 续体(Continuation)

协程中同样拥有续体的概念,Kotlin 中的定义为 挂起点之后的剩余应执行的代码,如在以下例子中,runTask() 便作为挂起函数 request() 这一挂起点的续体。

val result = request(arguments)
runTask(result)

Continuation 便是续体在 Kotlin 协程中的表现形式,它是一个用于回调的接口。而 CPS 转换,在命名上听起来非常高深,但本质上即将协程代码转换为等价的回调形式,如前文的 suspendingFunction() 函数声明会被编译器转换为以下形式:

fun suspendingFunction(a: Int, vararg b: String, $completion: Continuation<Unit>): Any?

就这?

仿佛一切魔法都被打破一般,协程的神奇之处仅此而已?但是,如果协程仅被单纯地转换为回调的形式,会有一个严重的问题:

如果每个续体都要创建一个类,那该如何解决类加载的大量资源损耗?

状态机

我们用上文提及的方法,看看下面这段代码的 Jvm 实现是什么:

suspend fun suspendingFunction(argument: String): Boolean {
    val result = mockRequest(argument)
    return result == 0
}