Continuation 到底是谁创建的?
最近在研究 Kotlin 协程反编译代码的时候,有一段代码让我困惑了很久。
几乎每一个 suspend 函数都会出现类似下面这样的代码(省略了一些无关内容):
ini
Continuation $continuation;
label20: {
if ($completion instanceof GetUserContinuation) {
$continuation = (GetUserContinuation) $completion;
if (($continuation.label & Integer.MIN_VALUE) != 0) {
$continuation.label -= Integer.MIN_VALUE;
break label20;
}
}
$continuation = new GetUserContinuation($completion);
}
第一次看到的时候,我一直有两个疑问。
第一个:
为什么这里要判断
$completion的类型?
第二个,也是让我困惑最久的:
$continuation到底是谁创建的?
如果你和我一样,只盯着这段代码看,很容易越看越迷糊。
真正理解它,需要先弄清楚一个问题:
一个 suspend 函数,到底会执行几次?
suspend 函数其实会"进入"两次
还是上一篇文章里的例子:
kotlin
suspend fun getUser(id: Int): User {
val realId = id + 1
delay(1000)
return User(realId, "User$realId", "user$id@example.com")
}
如果 delay() 没有发生挂起,那这个函数只会执行一次。
但是现实情况是:
scss
第一次进入
↓
执行到 delay()
↓
挂起
↓
一秒以后恢复
↓
再次进入 getUser()
注意这里的"再次进入"。
不是继续执行。
而是重新调用了一次整个函数。
只是由于 label 的存在,它会直接跳到对应的 case。
上一篇文章我们已经知道了这一点。
现在新的问题来了。
第一次进入和恢复进入,使用的是同一个 Continuation 吗?
答案是:
是。
而上面的那段代码,就是为了区分这两种情况。
第一次进入函数
假设有这样一段代码:
ini
runBlocking {
val user = getUser(1)
}
编译以后,调用大致变成:
scss
getUser(1, continuation);
这里传进来的 continuation 并不是 getUser 自己的。
它属于调用者,也就是 runBlocking。
于是第一次进入函数的时候:
php
if ($completion instanceof GetUserContinuation)
结果是:
arduino
false
因为:
runBlocking 的 Continuation
≠
getUser 的 Continuation
于是编译器会创建一个新的对象:
ini
$continuation = new GetUserContinuation($completion);
这就是 getUser 自己的状态机。
所以第一次调用的时候:
scss
runBlocking
↓
runBlockingContinuation
↓
new GetUserContinuation(...)
到这里,第一个疑问就解决了。
每一个 suspend 函数第一次执行时,都会创建属于自己的 Continuation。
恢复的时候为什么没有重新创建?
继续执行:
scss
delay(1000)
发生挂起。
delay 会保存刚刚创建好的 GetUserContinuation。
一秒以后:
scss
Timer 到期
↓
resumeWith()
↓
invokeSuspend()
这时候会执行生成的代码:
kotlin
public Object invokeSuspend(Object result) {
this.result = result;
this.label |= Integer.MIN_VALUE;
return getUser(0, this);
}
注意最后一句:
kotlin
getUser(0, this)
这里传进去的,不再是 runBlocking 的 Continuation。
而是:
kotlin
this
↓
GetUserContinuation
于是再次进入函数:
php
if ($completion instanceof GetUserContinuation)
结果变成:
arduino
true
于是:
ini
$continuation = (GetUserContinuation)$completion;
直接复用之前创建好的对象。
不会再执行:
scss
new GetUserContinuation(...)
因为恢复的时候,之前保存的局部变量、label 等状态都还在这个对象里面。
如果重新创建一个对象,这些状态就全部丢失了。
为什么每个 suspend 函数都要创建自己的 Continuation?
看到这里,我又产生了一个新的疑问。
既然 runBlocking 已经有一个 Continuation 了,为什么不能所有函数共用一个?
后来继续往下看源码才发现,这是不可能的。
例如:
kotlin
suspend fun getUserInfo(id: Int): User {
val user = getUser(id)
val avatar = getAvatar(id)
val token = getToken(id)
return user.copy(
avatar = avatar,
token = token
)
}
这里实际上有四个 suspend 函数:
runBlocking
↓
getUserInfo
↓
getUser
↓
getAvatar
↓
getToken
每一个函数都有自己的局部变量。
例如:
getUser 需要保存:
bash
id
realId
而 getUserInfo 需要保存:
sql
user
avatar
token
它们需要保存的状态完全不同。
因此,每个 suspend 函数都会生成属于自己的 ContinuationImpl,专门负责保存自己的执行现场。
至于这些 Continuation 是如何连接起来、最终把结果一层层返回给调用者的,我们下一篇文章再继续分析。
小结
当时困扰我很久的问题,其实答案并不复杂。
反编译代码里的这段逻辑:
php
if ($completion instanceof GetUserContinuation) {
...
} else {
$continuation = new GetUserContinuation($completion);
}
实际上就是在区分两种情况:
第一次调用:
bash
调用者传入自己的 Continuation
↓
创建当前 suspend 函数自己的 Continuation
恢复执行:
scss
invokeSuspend()
↓
把之前创建好的 Continuation 再次传回来
↓
继续执行状态机
所以,一个 suspend 函数的 Continuation 只会创建一次。
以后无论挂起、恢复多少次,都会一直使用这个对象。
下一篇,我们继续回答另一个让我困惑了很久的问题:
为什么一个 suspend 函数创建了自己的 Continuation,却还能把结果返回给调用它的 suspend 函数?
这些 Continuation 之间到底是什么关系?