位置:首页 > Kotlin > Kotlin 协程原理拆解:suspend 为什么能挂起又恢复

Kotlin 协程原理拆解:suspend 为什么能挂起又恢复

时间:2026-08-25  |  作者:冻月看渠  |  阅读:0

目录

  1. 为什么 Kotlin 需要协程
  2. 从一个简单例子,看协程如何被编译
  3. 协程背后的三个关键点:挂起、恢复、继续执行
  4. 把协程原理压缩成一条可记住的流程
  5. 理解 Kotlin 协程时,最该记住什么

前言

很多 Kotlin 开发者会用协程,却未必说得清 `suspend` 为什么能把异步代码写成同步风格:线程为什么不会被阻塞,挂起后又凭什么能精准恢复。要把这些问题讲透,不能只停留在 API 用法上,而要顺着反编译后的代码往里看,弄明白状态机、`Continuation` 和 `label` 分别扮演什么角色。

很多 Kotlin 开发者都会先记住协程“好用”,却说不清它到底为什么成立:明明代码看起来是顺序执行,为什么遇到耗时任务时线程却不会被堵住?而一个被 suspend 修饰的函数,挂起之后又是如何准确回到原来的位置继续往下跑的?这篇文章就从这些最容易让人心里发虚的问题出发,先看协程解决了什么,再结合反编译代码拆开它背后的状态机逻辑,最后把挂起与恢复流程压缩成一条能直接带走的理解路径。

为什么 Kotlin 需要协程

在 Android 开发里,Kotlin 已经是官方第一语言。它提供了不少语法糖和高阶函数,但真正让大量业务代码写法发生变化的,往往还是协程。

Kotlin 协程与回调写法对比信息图
回调嵌套与协程顺序写法对比用并排结构对比传统回调嵌套与 suspend 顺序写法,突出协程首先改善的是代码组织方式。

协程最直观的价值,不是“更高级”,而是它把原本难读、难维护的异步回调,改写成了接近同步代码的形式。开发者照着业务顺序往下写,编译器再去处理背后的挂起、恢复和回调衔接。

没有 suspend 时:回调层层嵌套

如果不使用 suspend,一段连续的异步流程通常会写成这样:

fun fetchData(callback: (Result)-> Unit){
	fetchUser{ user->
		 fetchProfile(user){ profile ->
		 		saveToDatabase(profile){ result->
		 			callback(result) //嵌套越来越深
		 		}
		}
	}
}

问题很明显:代码不是按业务顺序展开,而是被迫按回调层级展开。流程一长,就容易出现所谓的“回调地狱”,阅读、调试和异常处理都会变得别扭。

有 suspend 时:异步流程写成同步风格

换成挂起函数后,同样的逻辑可以写成更接近自然思路的样子:

suspend fun fetchData(): Result{
	val user = fetchUser()  //这里实际是异步的
	val profile = fetchProfile(user)
	return saveToDatabase(profile)
}

代码从上到下就是业务执行顺序,但这里真正值得追问的是三件事:

  1. 协程怎么做到执行耗时任务,却又不阻塞线程?
  2. 被挂起的函数,之后怎么恢复?
  3. 恢复之后,又为什么能自动接着执行后面的挂起函数?

要回答这几个问题,就不能只停留在“协程很好用”这个层面,而要看到 suspend 经过编译之后到底变成了什么。

从一个简单例子,看协程如何被编译

先看一个很小的协程例子:在协程内部连续执行两个耗时的挂起函数。

package com.ssz.kotlindemo.grammar

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.ssz.kotlindemo.R
import kotlinx.coroutines.*

class CoroutineActivity : AppCompatActivity(){
   override fun onCreate(savedInstanceState: Bundle) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_coroutine);

       CoroutineScope(Dispatchers.IO).launch {
           Log.d("sszLog", "开始")
           delay(2000) //耗时任务
           delay(1000) //耗时任务
           Log.d("sszLog:", "结束")
       }

   }

}

这段 Kotlin 代码本身并不复杂,但理解协程原理的关键,不在源码表面,而在编译结果。

在 Android Studio 里,可以通过下面的路径查看 Kotlin 生成的字节码:

Tools -> Kotlin -> Show Kotlin Bytecode

字节码不太适合直接阅读,一般还会进一步点击 Decompile 反编译成 Java,再观察编译器到底生成了什么结构。反编译后能看到类似下面的代码:

package com.ssz.kotlindemo.grammar;

import android.os.Bundle;
....忽略...

public final class CoroutineActivity extends AppCompatActivity {
   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(1300025);
      BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getIO()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
         int label;

         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            label17: {
               Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               switch(this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  Log.d("sszLog", "开始");
                  this.label = 1;
                  if (DelayKt.delay(2000L, this) == var2) {
                     return var2;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);//主要是用于检查上一个挂起函数是否正常执行
                  break;
               case 2:
                  ResultKt.throwOnFailure($result);
                  break label17;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
               }

               this.label = 2;
               if (DelayKt.delay(1000L, this) == var2) {
                  return var2;
               }
            }

            Log.d("sszLog:", "结束");
            return Unit.INSTANCE;
         }

         @NotNull
         public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
            Intrinsics.checkNotNullParameter(completion, "completion");
            Function2 var3 = new (completion);
            return var3;
         }

         public final Object invoke(Object var1, Object var2) {
            return (()this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
         }
      }), 3, (Object)null);
   }
}

即使不逐行深究,也能先抓到几个最重要的信号:

  • 协程代码被改写成了一个带 label 的结构。
  • 核心执行入口不再只是普通函数调用,而是围绕 invokecreateinvokeSuspend 展开。
  • 每遇到一个挂起点,都会配合 label 保存当前位置。
  • delay(2000L, this) 这样的调用,把当前对象 this 传了进去,而这个 this 正是后续恢复执行的关键。

协程背后的三个关键点:挂起、恢复、继续执行

把上面反编译后的代码拆开看,协程运行的核心其实可以归结为三个问题,也对应三个关键机制。

Kotlin 协程状态机执行流程信息图
协程状态机如何完成挂起与恢复把 launch 到 invokeSuspend 的入口链路,以及。

入口是怎样跑起来的

从调用链上看,协程不是直接“神奇地执行”,而是被框架按既定入口启动:

协程框架 → invoke() → create() → 新状态机实例 → invokeSuspend()

调用栈可以看到这条链路:

at BuildersKt.launch$default(BuildersKt.kt:-1) // 生成的桥接方法
at BuildersKt.launch(Builders.kt:60) // 实际的launch方法 at AbstractCoroutine.start(AbstractCoroutine.kt:112) // 启动协程 at CoroutineStart.invoke(CoroutineStart.kt:145) // 启动模式

这里最值得注意的是两步:

BuildersKt.launch 会通过协程,CoroutineStart 去调用 invoke(Object var1, Object var2) ,
然后执行 (()this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);  

当走到这里,有两个关键点:
1、通过 create(var1, (Continuation) var2)  创建状态机实例. (这个在后面很有用)
2、调用 invokeSuspend 

也就是说,协程体在编译后,已经不再是“普通代码块”,而是被包装进了一个状态机对象里。后续每次继续执行,本质上都是再次调这个状态机。

为什么不会阻塞线程

先看最关键的一段:

this.label = 1;
if (DelayKt.delay(2000L, this) == var2) {
    return var2;
}

这里的 var2 实际上就是 COROUTINE_SUSPENDED。意思是:如果当前调用的是一个挂起函数,并且它需要先挂起当前协程,那么就立即返回这个标记值。

if (DelayKt.delay(2000L, this) == var2) {//这里var2其实是COROUTINE_SUSPENDED
     return var2;
}

这一步非常关键。它不是把线程堵在那里死等,而是直接把当前协程让出去。线程因此可以被释放出来,继续处理别的任务,这就是协程“看起来顺序执行,但不会阻塞线程”的关键原因。

准确地说,挂起的是协程,不是线程。线程只是执行到这里发现需要等待,于是先退出当前协程的后续流程,等条件满足后再回来继续。

挂起之后为什么还能恢复

再看同一行调用里的另一个重点:

DelayKt.delay(2000L, this)

这里把 this 传给了 delay。而这个 this,不是普通业务对象,而是当前的状态机实例。它本身承担了“我执行到哪了、之后该从哪接着跑”的职责。

delay 这个挂起函数执行结束后,就会通过这个状态机对象触发恢复逻辑,也就是调用 resumeWith,然后重新回到 invokeSuspend

所以,协程之所以能恢复,不是因为它“自动记住了上下文”这么抽象,而是因为编译器已经把现场保存进了状态机里,并把这个状态机作为 Continuation 传给了挂起函数。

恢复后为什么会自动执行后面的代码

恢复之后能接着往下跑,关键靠的是 label

在第一次调用挂起函数之前,代码已经先做了这一步:

this.label = 1;

它相当于先把“下次回来要去哪里”记下来。等 delay 完成后,再次进入 invokeSuspend,就会根据当前的 label 值进入对应分支:

  • label = 0:第一次进入,执行开头逻辑。
  • label = 1:说明第一个 delay(2000) 已经完成,接着往后执行。
  • label = 2:说明第二个 delay(1000) 也完成了,进入收尾逻辑。

这样一来,每一个挂起点都像是在状态机里打了一个坐标。恢复时并不是从头执行,而是准确跳回上一次记录的位置,继续执行后面的代码。

把原文里的三个问题重新收束一下,就是:

1、协程怎么做到,执行耗时任务,却又不阻塞线程?

delay(2000L, this) == var2 如果发现是挂起函数,会直接 return var2,
这也就是所谓的释放当前的线程,这也就是不会阻塞线程的关键。

2、被挂起的函数,又怎么重新恢复?

DelayKt.delay(2000L, this) 会把状态机,传递给耗时任务,
执行完再次回调invokeSuspend,回到原来挂起的地方。

3、恢复后,怎么自动去执行后面的挂起函数?

就是主要是通过label 这个状态,通过每执行完一次挂起函数,状态就递增,
当 invokeSuspend 再次被调用,就可以执行下一个挂起函数。

把协程原理压缩成一条可记住的流程

如果前面的反编译代码还是显得有点重,可以把它再压缩一层,只看“怎么挂起”和“怎么恢复”。

Kotlin 协程简化模型信息图
用简化模型看懂协程本质把复杂反编译结果压缩成状态机实现 Continuation 的简化模型。

怎么挂起

怎么挂起的呢:
状态机{ //实现了 Continuation
	invokeSuspend(){
		case 0: 
			this.label = 1 //设置下一个状态
			if(delay(2000L, this) == COROUTINE_SUSPENDED){ //传递this(也就是 Continuation)
				return COROUTINE_SUSPENDED; //挂起 (有耗时的任务,先不去理会它,这是挂起)
			}
		case 1:
		...
	}
	resumeWith(){//这个是Continuation 中的方法,因为状态机实际上是实现了 Continuation,自然就有这个方法了
	}
}

这个简化版已经足够说明问题:协程并不是“停在原地等”,而是先记录状态,再把控制权交出去。

怎么恢复

怎么恢复的呢:
//恢复时
delay(long milliseconds, Continuation continuation){//执行完耗时,就会调用resumeWith
	continuation.resumeWith(Result.success(Unit)) 
}	
resumeWith(){
	//this其实就是状态机,也就是进行了回调,这就是为什么,挂起为什么能够恢复的关键。
	this.invokeSuspend()//此时label = 1,就会去执行下一个挂起函数
}

恢复的本质也就一句话:耗时任务完成后,通过传进去的 Continuation 回调状态机,状态机再根据当前 label 继续往下执行。

把状态机理解成 Continuation,会更容易看懂

如果还想再往前走一步,可以把编译器生成的结构,近似理解成一个实现了 Continuation 的类:

// 我们的状态机类可以这么去理解,其实是实现了Continuation接口
public class CoroutineStateMachine implements Continuation {
    int label;
    
    public void resumeWith(Result result) { //因为实现 Continuation,也就有了这个方法
        Object outcome = this.invokeSuspend(result);
        // ...
    }
    
    // 状态机逻辑
    public Object invokeSuspend(Object result) {
        switch(this.label) {
            // 状态逻辑...
        }
    }
}

这样理解之后,很多行为都会变得顺理成章:

  • 为什么会有 resumeWith:因为状态机本身实现了 Continuation
  • 为什么恢复时会重新进 invokeSuspend:因为恢复本质上就是再次调度这个状态机。
  • 为什么能从上次的位置继续:因为 label 已经提前记录了状态。

所以,从实现上说,协程并不是凭空消灭了回调,而是把回调、状态保存、恢复入口这些繁琐工作交给编译器和协程框架去处理。开发者看到的是顺序代码,底层做的仍然是状态机加回调这一套机制。

理解 Kotlin 协程时,最该记住什么

如果只保留最核心的结论,可以记住下面几条:

  1. suspend 不是简单的语法标记,它会触发编译器把协程代码改写成状态机。
  2. 协程不会阻塞线程,关键在于遇到挂起点时返回 COROUTINE_SUSPENDED,把当前线程让出来。
  3. 挂起之所以能恢复,是因为状态机作为 Continuation 被传给了挂起函数,任务完成后会回调 resumeWith
  4. 恢复后能继续往下执行,是因为 label 记录了当前状态,再次进入 invokeSuspend 时会跳到对应分支。

换句话说,协程让你写出“像同步一样的异步代码”,靠的并不是运行时魔法,而是编译器生成的状态机、挂起标记以及恢复回调共同配合。把这条主线想明白之后,再去看 launchasyncwithContext 这些更常用的协程 API,心里会稳很多。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多