Continuation 到底是谁创建的?

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 之间到底是什么关系?

相关推荐
峥嵘life7 小时前
Android16 311Y3 EAP-TLS 网络连接失败分析与修复总结
android·开发语言·人工智能·php
Kapaseker8 小时前
破坏性更新 - 解读 Jetpack Compose 1.12
android·kotlin
雨白9 小时前
我的 UML 学习笔记:结合 Android 实例看懂 4 种常用图表
android·架构
怣疯knight9 小时前
快速判断apk有没有支持16kb页面标准
android
风流 少年10 小时前
Spring AI 2.0:Advisor
android·人工智能·spring
YM52e11 小时前
分页查询的基石:ArkTS 为鸿蒙商品列表设计 LIMIT/OFFSET 的表
android·学习·华为·harmonyos
安卓与AI研习社13 小时前
Android ANR 到底怎么定位?从触发原理到日志判断的完整方法
android
阿pin15 小时前
Android随笔-MVI
android·mvi
leobertlan15 小时前
【好玩系列】训练一个神经网络指导小孩玩游戏
android·机器学习·程序员
Android-Flutter16 小时前
android LeakCanary 工作原理 详解
android·kotlin