协程为 Kotlin 提供了更优雅的异步编程方式,但由于 Jvm 本身并没有提供协程支持,因此 Kotlin/Jvm 中的协程仿佛如魔法一般的存在。
这一次我们将对协程的实现原理进行解析。
挂起函数
函数声明转换
Kotlin 从语言层面提供了 suspend
关键字,用于表示该函数为挂起函数,但在 Jvm 中并没有可挂起的概念。
以下面的一个挂起函数声明为例:
suspend fun suspendingFunction(a: Int, vararg b: String)
- 1
如果通过 IDEA 将其编译的 Class 文件进行反编译,可以得到一个与其 Jvm 实现等价的 Java 方法声明:
public static final Object suspendingFunction(int a, @NotNull String[] b, @NotNull Continuation $completion)
- 1
挂起函数的 suspend
关键字转换成了一个额外的参数,其类型为 Continuation
。这一操作是 Kotlin 编译器在编译期执行的,称为 CPS(Continuation-Passing Style,续体传递风格)转换,而在下一步分析协程实现原理之前,需要先对 “续体” 这一概念进行说明。
续体
采用回调的方法获取耗时操作的结果一般会类似如下例子中的形式:
request(arguments) { result ->
runTask(result)
}
- 1
- 2
- 3
在上述例子中,Lambda 作为这一代码片段中 request()
函数执行完毕的后续操作,这一部分被称为 续体(Continuation)。
协程中同样拥有续体的概念,Kotlin 中的定义为 挂起点之后的剩余应执行的代码,如在以下例子中,runTask()
便作为挂起函数 request()
这一挂起点的续体。
val result = request(arguments)
runTask(result)
- 1
- 2
Continuation
便是续体在 Kotlin 协程中的表现形式,它是一个用于回调的接口。而 CPS 转换,在命名上听起来非常高深,但本质上即将协程代码转换为等价的回调形式,如前文的 suspendingFunction()
函数声明会被编译器转换为以下形式:
fun suspendingFunction(a: Int, vararg b: String, $completion: Continuation<Unit>): Any?
- 1
就这?
仿佛一切魔法都被打破一般,协程的神奇之处仅此而已?但是,如果协程仅被单纯地转换为回调的形式,会有一个严重的问题:
如果每个续体都要创建一个类,那该如何解决类加载的大量资源损耗?
状态机
我们用上文提及的方法,看看下面这段代码的 Jvm 实现是什么:
suspend fun suspendingFunction(argument: String): Boolean {
val result = mockRequest(argument)
return result == 0
}
- 1
- 2
- 3
- 4