Kotlin 协程的挂起(suspend)原理

用了几年协程,你真的理解 suspend 背后发生了什么吗?

一、一个简单的例子

我们先写一个最普通的 suspend 函数:

kotlin 复制代码
// Test.kt
suspend fun fetchData() {
    println("step 1")
    delay(1000L)
    println("step 2")
}

二、反编译

编译之后,可以用 javap 命令反编译 class 文件查看编译结果:

bash 复制代码
javap -c -p TestKt.class
javap -c -p 'TestKt$fetchData$1.class'

编译器在编译期会生成一个匿名类(状态机)和一个静态方法(状态分发),如下:

java 复制代码
// 匿名类(状态机):
final class TestKt$fetchData$1 extends ContinuationImpl {

    Object result;     // 上一个 suspend 调用的返回值
    int label;         // 当前执行到哪个挂起点(0、1)

    public TestKt$fetchData$1(Continuation completion) {
        super(completion);
    }

    @Override
    public Object invokeSuspend(Object $result) {
        this.result = $result;
        this.label |= Integer.MIN_VALUE;
        return TestKt.fetchData(this);
    }
}

// 静态方法(状态分发):
public static final Object fetchData(Continuation<? super Unit> $completion) {

    // 第一步:创建或复用状态机对象
    TestKt$fetchData$1 sm;
    if ($completion instanceof TestKt$fetchData$1) {
        sm = (TestKt$fetchData$1) $completion;
        if ((sm.label & Integer.MIN_VALUE) != 0) {
            sm.label -= Integer.MIN_VALUE;
        }
    } else {
        sm = new TestKt$fetchData$1($completion);
    }

    // 第二步:状态分发 ------ 本质上就是 switch-case
    switch (sm.label) {
        case 0:   // 初始状态
            ResultKt.throwOnFailure(sm.result);
            System.out.println("step 1");
            sm.label = 1;
            if (delay(1000L, sm) == COROUTINE_SUSPENDED) return null;
            break;

        case 1:   // 从 delay 恢复后
            ResultKt.throwOnFailure(sm.result);
            System.out.println("step 2");
            return Unit.INSTANCE;           // 返回结果
    }
    throw new IllegalStateException("call to 'resume' before 'invoke'");
}

三、执行过程

3.1 调用入口

假设我们在协程作用域中调用了 fetchData()

经过协程的一系列调用和转换(scope.launch { fetchData() } → ... → startCoroutineCancellableresumeWith(Unit)),最终会调用到 TestKt$fetchData$1.invokeSuspend(Unit) 这个方法:

java 复制代码
// 前面生成的匿名类的方法
@Override
public Object invokeSuspend(Object $result) {
    this.result = $result;
    this.label |= Integer.MIN_VALUE;   // 标记"正在执行"
    return TestKt.fetchData(this);      // 调用状态分发器
}

fetchData(this) 中的 this 就是状态机本身

这时 label == 0,所以进入 tableswitchcase 0 分支:

java 复制代码
case 0:                     // 初始进入 fetchData,sm.label == 0
    ResultKt.throwOnFailure(sm.result);   // 检查上次是否异常
    System.out.println("step 1");
    sm.label = 1;           // ← 记录下一点:下次进来走 case 1
    if (delay(1000L, sm) == COROUTINE_SUSPENDED) {
        return null;        // ← 挂起!函数返回了
    }
    break;

线程没有被阻塞,方法调用从 fetchData 函数 return null 了就结束了。
sm 传给了 delay,相当于传过去了一个回调函数。
delay 内部开了个定时器,1 秒后回调 sm.resumeWith(Result.success(Unit)) 恢复执行。

3.2 再次调用 invokeSuspend

1 秒后定时器触发:

kotlin 复制代码
// delay 的实际行为(简化示意)
// delay 不阻塞线程,它往 Dispatcher 的定时器队列注册了一个回调
// 1 秒后 Dispatcher 触发该回调,恢复续体:
sm.resumeWith(Result.success(Unit))

// sm.resumeWith 内部执行了:
sm.invokeSuspend(Unit)

// sm.invokeSuspend(Unit) 内部执行了:
fetchData(sm)

此时 sm.label == 1,所以执行 label 1 的分支:

java 复制代码
case 1:                     // 从 delay 恢复后
    ResultKt.throwOnFailure(sm.result);   // 检查 Unit,正常
    System.out.println("step 2");
    return Unit.INSTANCE;                  // 执行完毕,返回结果

函数执行完了,Unit.INSTANCE 会一层层向上传,协程结束。

四、简单总结

  1. 协程挂起的本质还是回调,只是提供了语法糖,可以写出同步风格的代码。
  2. 挂起就是给别的函数传入了一个回调方法,然后直接 return,不阻塞线程。
  3. 回调的时候根据 label 来判断执行哪个 case 分支代码。
相关推荐
阿正的梦工坊10 小时前
Kotlin 面试题全面解析:从基础到进阶
android·开发语言·kotlin
赏金术士11 小时前
第一章:项目概述与环境搭建
android·kotlin·compose
Kapaseker11 小时前
Kotlin 解构新语法完全解析:从"看位置"到"看名字"
android·kotlin
阿正的梦工坊11 小时前
Kotlin 中的 ?. 和 . 语法详解
开发语言·python·kotlin
Fate_I_C11 小时前
View Binding与Data Binding 核心区别及实战指南
android·kotlin·viewbinding·databinding
阿正的梦工坊11 小时前
Kotlin:现代编程语言的优雅之选
android·开发语言·kotlin
YF02111 天前
深入剖析 Kotlin 的高效之道与核心实战
android·kotlin·app
逐光老顽童1 天前
Kotlin 委托机制完全指南:从语法糖到架构实战
kotlin
逐光老顽童1 天前
Kotlin协程详解与现代Android开发实践
kotlin