续体和状态机 —— suspend 函数的 CPS 变换

如果你看过 kotlin 挂起函数反编译后的 java 代码,你会发现短短 5 行代码反编译出 50 行代码。你去问 AI 它会告诉你这是 CPS 变换,今天来拆解一下挂起函数的本质。

一. 挂起函数的本质

挂起函数的实现本质上就是编译器对代码的 CPS 变换,你可以问问 AI CPS 全称是什么,它会告诉你是 Continuation-Passing Style 也就是 续体传递风格。在编译器,编译器会以每个挂起点为分界,把协程体分成状态机片段,每个片段对应一个回调续体(准确来说是同一个,只是 label 不同)。

简单点来说,你写的一段连续代码,编译器把它切成几段,每段执行完后记录"我执行到哪里了",然后挂起等待;等到条件满足(比如网络请求有了结果)通过之前保存的续体恢复执行下一段。

协程执行时,在挂起点之前只是正常执行并返回挂起标记,不会触发任何回调;只有在挂起任务完成、通过续体的 resume 恢复执行时,才会触发一次回调,进入下一个状态继续执行。

二. 反编译代码解析

真正要去理解续体和状态机,最简单的方法我觉得就是去写一个挂起函数,在里面去制造挂起点,自己真正的去看看反编译后的 java 代码。

kotlin 复制代码
suspend fun myTest() {
    println("A")
    delay(1000)
    println("C")
}

反编译后的代码如下

kotlin 复制代码
@Metadata(
   mv = {2, 0, 0},
   k = 2,
   xi = 48,
   d1 = {"..."},
   d2 = {"myTest", "", "(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", "..."}
)
public final class CoroutineKt {
   @Nullable
   public static final Object myTest(@NotNull Continuation $completion) {
      Continuation $continuation;
      label20: {
         if ($completion instanceof <undefinedtype>) {
            $continuation = ()$completion;
            if (($continuation.label & Integer.MIN_VALUE) != 0) {
               $continuation.label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         $continuation = new ContinuationImpl($completion) {
            // $FF: synthetic field
            Object result;
            int label;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return CoroutineKt.myTest((Continuation)this);
            }
         };
      }

      Object $result = $continuation.result;
      Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch ($continuation.label) {
         case 0:
            ResultKt.throwOnFailure($result);
            System.out.println("A");
            $continuation.label = 1;
            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");
      }

      System.out.println("B");
      return Unit.INSTANCE;
   }
}

代码虽然很长,但是可以分成三块来理解:

1. 方法签名变换

原来的 suspend fun myTest() 编译后变成了:

java 复制代码
public static final Object myTest(@NotNull Continuation completion)

这个 $sompletion 是调用者自己的 Continuation。它自己没有 label 等状态,他只是被当做了"上下文传入",编译器会基于它生成内部真正的状态机。 返回类型从 Unit 变成了 Object。你可以看到反编译后的代码其实有两种返回值:

  • COROUTINE_SUSPENDED ------ 表示函数未执行完毕,暂时挂起
  • Unit.INSTANCE ------ 执行完毕

一句话去总结的话,suspend 函数编译后等价于一个接收 Continuation、返回 Object 的回调风格函数。调用者通过传入自己的 Continuation 来接受执行结果。

三. Continuaton 到底是啥

想知道 Continuation 是什么最直白的方法就是去看看

java 复制代码
@SinceKotlin("1.3")
public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

可以看到 Continuation 其实是一个接口,其中定义了一个 resumeWith() 方法,从语义上来看好像是定义了一个恢复执行的方法?

反编译之后的代码里其实还有一个很关键的匿名内部类 ContinuationImpl,去看它的源码可以看到它其实继承自 BaseContinuationImpl,接着看 BaseContinuationImpl 继承自 Continuation,而其中就有着 resumeWith() 方法通用逻辑。

kotlin 复制代码
public final override fun resumeWith(result: Result<Any?>) {
    // 通过循环展开递归,让恢复调用链的栈更短、更清晰
    var current = this
    var param = result
    while (true) {
        probeCoroutineResumed(current)
        with(current) {
            val completion = completion!! // 快速失败:没有 completion 直接抛
            val outcome: Result<Any?> =
                try {
                    val outcome = invokeSuspend(param)
                    if (outcome === COROUTINE_SUSPENDED) return
                    Result.success(outcome)
                } catch (exception: Throwable) {
                    Result.failure(exception)
                }
            releaseIntercepted() // 当前状态机实例结束
            if (completion is BaseContinuationImpl) {
                // 通过循环展开递归
                current = completion
                param = outcome
            } else {
                // 到达顶层 completion,调用并返回
                completion.resumeWith(outcome)
                return
            }
        }
    }
}

这个方法就是续体恢复的调度引擎 。它通过 while(true) 循环和 invokeSuspend 调用不断推进状态机,直到所有 Continuation 链执行完毕。

最终得到结论:无论协程是首次启动还是从挂起恢复,最终都会通过 resumeWith → invokeSuspend → myTest 这条链进入状态机。 区别只在于进入时 label 的值不同。

说人话就是:挂起函数每次被调用都会有一个 Continuation 传入,这是编译器和框架层帮我们做好的。至于 Continuation 的作用你接着往下看完状态机想必就会有一个完整的挂起函数的链条!

五. 状态机

反编译代码中的 label20 是整个状态机的门卫,它的职责就是判断当前是首次调用还是挂起后恢复。

java 复制代码
label20: {
   if ($completion instanceof <undefinedtype>) {
      $continuation = ()$completion;
      if (($continuation.label & Integer.MIN_VALUE) != 0) {
         $continuation.label -= Integer.MIN_VALUE; // 清除恢复标记
         break label20; // 直接复用已有的 $continuation,跳过新建
      }
   }

   // 首次调用:创建新的状态机实例
   $continuation = new ContinuationImpl($completion) {
      Object result;
      int label;

      public final Object invokeSuspend(@NotNull Object $result) {
         this.result = $result;
         this.label |= Integer.MIN_VALUE;
         return CoroutineKt.myTest((Continuation)this);
      }
   };
}

这里的 <undefinedtype> 实际上是编译器生成的内部类,也就是当前 suspend 函数专属的状态机类名。

  • 首次调用:$completion 不是本函数的状态机实例,创建新的 ContinuationImpl 匿名内部类,label 初始值为 0
  • 恢复调用:$completion 是本函数的状态机实例(instanceof 通过),label 最高位为 1 ,清除标记后 break label20,然后复用已有实例。

接下来才是重中之重------ switch 状态机:

java 复制代码
Object $result = $continuation.result;
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch ($continuation.label) {
   case 0:
      ResultKt.throwOnFailure($result);
      System.out.println("A");            // 打印 A
      $continuation.label = 1;            // 更新状态为 "下一段"
      if (DelayKt.delay(1000L, $continuation) == var3) {
         return var3;                     // 返回 COROUTINE_SUSPENDED
      }
      break;
   case 1:
      ResultKt.throwOnFailure($result);
      break;                              // 跳出 switch
   default:
      throw new IllegalStateException(...);
}

System.out.println("B");                  // 打印 B
return Unit.INSTANCE;                     // 正常结束

其实到这里已经很明了了,拿到续体的 label,然后根据 label 来决定执行哪个 case,比如第一次执行该函数,label 初始化为 0,执行 case 0:,打印 A 之后,设置 label = 1,再去挂起 1000L,此时续体保存在堆上,label = 1,下次恢复接着执行该函数,传入该续体,跳过初始化复用续体,然后根据 label == 1 执行到 case 1:,braek 然后 println("B")。

四. 完整执行链路:两次调用的全貌推演

之前在自己的技术博客已经总结过了其实,截图放这了,接下来我用最最最最直白的话告诉你们挂起函数的真相!

首先挂起函数挂起的真正秘密其实就是 return,这样当前线程的方法栈中就弹出了这个栈帧,然后接着执行其他方法,这其实就是挂起函数不阻塞当前线程的秘密。

只不过 return 之前框架层会帮我们保存续体的实例。也就是代码上次执行到哪里了,然后等到约定的时间到了,框架层再次调用该函数并且把上次保存的续体传入,函数内部 switch 代码块根据续体的 label 去决定执行哪一块代码。

五. 大火收汁

  • suspend 函数 ≠ 普通函数: 编译后参数多了 Continuation,返回值变成了 Object。这是 CPS 变换的产物,将"连续代码"变成了"可中断的状态机"。
  • Continuation 是挂起函数的核心: 它持有状态机的一切信息------当前执行到哪了(label)、上次运行结果(result)、调用者是谁(completion)。它是协程"暂停后能继续"的根本原因。
  • label 是状态机的程序计数器: 编译器将协程体按挂起点切分,每个挂起点对应一个 case。label 记录当前执行到哪个阶段,每次进入方法时根据 label 跳转到对应位置。
  • Integer.MIN_VALUE 是恢复调用的暗号: invokeSuspend 将 label 最高位置 1,告诉 myTest "这是恢复调用,不是首次启动"。label20 块据此决定是创建新状态机还是复用已有实例。
  • COROUTINE_SUSPENDED 是挂起信号: 它不是一个异常也不是错误,而是一个特殊返回值,告诉上层"我没执行完,先把我挂起来"。resumeWith 看到它就 return,控制权交回事件循环。
  • resumeWith 的 while 循环实现"链式冒泡": 每层 Continuation 执行完毕后,通过 while 循环自动将结果传递给父 Continuation,避免递归调用造成的深栈问题。

协程这一块其实感觉还有很多地方要接着深入,越是了解越是感觉设计的精妙,最大的感悟就是 我也能做到,但是做不到。那些强大的开发者用我们也接触到的代码去做出了如此强大的机制,希望有一天自己也可以变得很他们有一强大吧。

相关推荐
WAsbry1 小时前
Flow数据模型:冷流、状态、事件与共享策略
android
WAsbry1 小时前
深入理解 Kotlin suspend:挂起语义、状态机与恢复调度
android
WAsbry1 小时前
Flow在Android中的完整落地:异常、重试与生命周期
android
WAsbry1 小时前
协程共享状态:Atomic、Mutex、Semaphore与状态封闭
android
WAsbry1 小时前
Flow执行模型:上下文保持与生产消费节奏
android
WAsbry1 小时前
协程任务的组织方式:launch、async、withContext与结构化并发
android
WAsbry1 小时前
本文区分原子变量、复合操作、并发数量与状态所有权四类问题,比较Atomic、Mutex、Semaphore和状态封闭的适用边界,并结合计数、缓存和限流场景,建立
android
2501_915909062 小时前
iOS test 测试怎么做?功能、性能、兼容、稳定与安全五类测试指南
android·ios·小程序·https·uni-app·iphone·webview
赵广陆2 小时前
企业实战:Markdown图片检索
android·java·开发语言