使用 Kotlin 协程绕不开 suspend(挂起)函数,那么 suspend 函数的本质是什么?
在这篇文章中我尝试讲讲我所理解的 suspend 函数
我们仍然可以通过反编译 Kotlin 代码来窥探 suspend 函数的细节
kotlin
fun main() {
runBlocking {
doSuspend()
}
}
suspend fun doSuspend() {
delay(1000L)
println("doSuspend")
}
这段简单的代码反编译成 Java 代码后是这个样子的
less
@Nullable
public static final Object doSuspend(@NotNull Continuation $completion) {
...
}
emm, 具体的实现我们先不看,因为信息量有点多...,容易被劝退
首先,我们可以确认的是 suspend 函数也是一个普通的函数
其次,它的内部实现被 Kotlin 编译器插件动过手脚...,帮我们生成了很多样板代码
再次,编译器插入了一个类型为 Continuation 的 completion 参数,这个 Continuation 在之前的系列文章提及过,可以想象成是对状态机(State Machine)的封装,而(suspend)函数可以认为是一个状态机
参数的命名也是有讲究的,completion,即完成的意思,最终执行结果通过它回调出去
你可以联想一下在「史前」时代我们如何封装一个异步方法
kotlin
fun doSuspend(callback: Callback) {
...
}
给函数增加一个 callback,以便在它干完事后通知一下我们,所以你也可以将 Continuation 和 Callback 进行类比,降低接收新事物的难度
最后总结一下:suspend 函数是一个带有 completion (回调) 尾部参数的函数
基于这个理解,我们可以写出下面这个搞怪的代码,当然只是为了验证我们的推断
kotlin
fun main() {
(::doSuspendMock as (suspend () -> Unit)).startCoroutine(
Continuation(
context = EmptyCoroutineContext,
resumeWith = {
println("ok, get!")
}
)
)
}
fun doSuspendMock(completion: Continuation<Unit>) {
println("doSuspendMock")
}
doSuspendMock 并不是 suspend 函数(没有 suspend 关键字),我可以把它强制转换成 suspend fun
startCoroutine 是 Kotlin 协程框架提供的方法,用于执行一个 suspend 方法(启动协程),这里我们手动创建了一个 Continuation(回调)用于接收 suspend 方法执行结果:
arduino
doSuspendMock
ok, get!
ok,我们接着看看之前说的信息量比较大的部分...
这段代码初见不好理解,需要你把自己想象成一台「电脑」,然后沉浸式的体验一下执行流程
任何的技术文章的解读都不及你自己静下心来在脑袋里跑一遍,所以我直接在代码上做点关键性的注释
php
@Nullable
public static final Object doSuspend(@NotNull Continuation $completion) {
Continuation $continuation;
// 这个便签内的代码用于创建和维护 Continuation(Callback)
label20: {
if ($completion instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)$completion;
if (($continuation.label & Integer.MIN_VALUE) != 0) {
$continuation.label -= Integer.MIN_VALUE;
break label20;
}
}
// 首次执行时,上面那个 if 条件不满足,所以会创建一个 Continuation 对象
// 它会贯穿整个 suspend 函数的生命周期
// 想想为什么这里需要一个 Continuation?为了接收 delay 的回调
// delay 也是一个 suspend 函数,在它执行完之后通知我们继续执行...
$continuation = new ContinuationImpl($completion) {
// $FF: synthetic field
Object result;
// 你可以把这个 label 想象成状态机的状态,初始值为 0
int label;
// 你可以把这个 invokeSuspend 函数想象成状态机在执行状态迁移
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
// 所谓的状态转移,就是重复的调用自个儿 doSuspend
return SuspendKt.doSuspend((Continuation)this);
}
};
}
// 这个 switch 就是所谓的状态机,因为我们只调用了一个 suspend 函数(delay)
// 分支会比较少,同时结构也比较简单
Object $result = $continuation.result;
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch ($continuation.label) {
case 0:
ResultKt.throwOnFailure($result);
// 将状态设置 1,下次回调 doSuspend 的时候,走 case 1
$continuation.label = 1;
// delay 函数也是一个 suspend 函数,continuation 用于接收回调
// 这里是实现挂起的细节,可以细品
if (DelayKt.delay(1000L, $continuation) == var3) {
return var3;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
// ok,我们的代码终于出现了
System.out.println("doSuspend");
return Unit.INSTANCE;
}
总结 suspend 函数由两部分组成,无论多复杂都是如此:
- Continuation(回调)初始化
- 状态机
suspend 函数内部如果调用别的 suspend 函数,它们会将 caller「分割」成多个状态
suspend 函数的执行就是在状态机内部打转,然后通过 completion 回调出去
顺便提一句谨慎使用 suspend,Kotlin 编译器插件夹带的私货有点多
Android Studio/Intellij IDE 中如果你声明的 suspend 函数并不会挂起它会建议你删除 suspend 关键字!