浅析协程与挂起函数实现原理

要理解suspend function(挂起函数)的原理,我们可以从一个生活故事开始,再结合代码和时序图逐步拆解。

一、生活故事:小明的 "暂停 - 继续" 做饭任务

想象小明要做一顿早餐,步骤是:

  1. 煮水(需要 5 分钟,期间不用一直盯着);
  2. 水开后煮面条(3 分钟,期间可以做别的);
  3. 煮面条时煎个蛋(2 分钟,无需等待)。

如果小明是个 "阻塞型" 选手,他会站在水壶前等 5 分钟,再站在锅前等 3 分钟 ------ 全程啥也干不了,效率极低。

但小明是 "挂起型" 选手:

  • 他把水壶放上去,设置了一个定时器(记着 "水开后要煮面条"),然后去准备煎蛋的材料;
  • 5 分钟后定时器响了,他回来继续煮面条,同时又设置了一个定时器(记着 "面条熟了要关火"),接着去煎蛋;
  • 3 分钟后定时器再响,他回来关火,早餐完成。

这里的 "煮水" 和 "煮面条" 就是挂起操作:执行到这里时,任务会 "暂停",但会留下一个 "继续执行的线索"(定时器 + 接下来的步骤),之后可以去做别的事,直到条件满足再 "恢复" 执行。

二、挂起函数的本质:不是阻塞,是 "有记忆的暂停"

在 Android 中,suspend函数的核心是挂起时不阻塞线程,且能记住暂停时的状态,之后恢复执行

比如一个简单的挂起函数:

kotlin 复制代码
// 挂起函数:模拟"煮水"(需要5秒)
suspend fun boilWater(): Boolean {
    delay(5000) // 挂起5秒(类似等水开)
    return true // 水开了
}

// 挂起函数:模拟"做早餐"
suspend fun makeBreakfast() {
    val waterReady = boilWater() // 第一步:煮水(挂起)
    if (waterReady) {
        delay(3000) // 第二步:煮面条(挂起)
        println("早餐做好了!")
    }
}

这个代码看起来和普通函数没区别,但编译器会对suspend函数做特殊处理 ------ 这才是关键。

三、编译器的 "魔法":CPS 转换与状态机

suspend函数之所以能 "暂停和恢复",是因为编译器会把它转换成带 "记忆" 的状态机,核心是两步:

1. CPS 转换:给函数加个 "继续执行的线索"

CPS(Continuation Passing Style,延续传递风格)是指:编译器会给suspend函数增加一个Continuation参数,用来保存 "暂停后要继续执行的逻辑"。

比如原函数boilWater(),编译后会变成类似这样(简化版):

kotlin 复制代码
// 编译器生成的函数:增加Continuation参数
fun boilWater(continuation: Continuation<Boolean>): Any? {
    // 执行delay(5000),并把"继续执行的逻辑"交给continuation
    return delay(5000, object : Continuation<Unit> {
        override fun resumeWith(result: Result<Unit>) {
            // 5秒后恢复:调用continuation,告诉它"水开了"
            continuation.resume(true)
        }
    })
}

Continuation就像小明的 "定时器 + 待办清单":保存了暂停时的状态(比如局部变量、下一步要执行的代码),等挂起操作完成后,通过resume方法触发继续执行。

2. 状态机:处理多个挂起点

如果一个suspend函数有多个挂起点(比如makeBreakfast里调用了两次delay),编译器会生成一个状态机类 ,用state变量记录当前执行到哪个阶段。

比如makeBreakfast编译后会变成类似这样(简化版):

kotlin 复制代码
// 编译器生成的状态机类
class MakeBreakfastStateMachine(
    private val continuation: Continuation<Unit> // 外部的继续逻辑
) : Continuation<Any?> {
    var state: Int = 0 // 状态:0-初始,1-煮水后,2-煮面条后
    var waterReady: Boolean = false // 保存局部变量

    override fun resumeWith(result: Result<Any?>) {
        try {
            when (state) {
                0 -> {
                    // 第一次执行:开始煮水
                    state = 1 // 下次恢复时从状态1开始
                    // 调用boilWater,并把自己作为continuation传入
                    boilWater(this)
                }
                1 -> {
                    // 从煮水恢复:获取水开的结果
                    waterReady = result.getOrThrow() as Boolean
                    if (waterReady) {
                        state = 2 // 下次恢复时从状态2开始
                        // 开始煮面条
                        delay(3000, this)
                    }
                }
                2 -> {
                    // 从煮面条恢复:完成早餐
                    println("早餐做好了!")
                    // 通知外部的continuation:任务完成
                    continuation.resume(Unit)
                }
            }
        } catch (e: Exception) {
            continuation.resumeWithException(e)
        }
    }
}

// 编译后的makeBreakfast函数:创建状态机并启动
fun makeBreakfast(continuation: Continuation<Unit>): Any? {
    return MakeBreakfastStateMachine(continuation).apply { resumeWith(Result.success(Unit)) }
}
  • state=0:初始状态,开始执行第一步(煮水);
  • state=1:从煮水恢复后,执行第二步(煮面条);
  • state=2:从煮面条恢复后,完成整个流程。

状态机通过state记录进度,通过成员变量保存局部变量(如waterReady),实现了 "暂停后记住状态,恢复后继续执行" 的效果。

四、时序图:完整流程拆解

用时序图展示makeBreakfast的执行过程(简化版):

text 复制代码
调用方 -> makeBreakfast() 
    │
    ├─ 创建状态机(state=0)并启动 
    │
    ├─ 状态机(state=0)-> boilWater(状态机自身)  // 开始煮水
    │
    ├─ boilWater -> delay(5000, 内部continuation)  // 挂起5秒,释放线程
    │
    │  (线程空闲,去处理其他任务,比如UI刷新)
    │
5秒后 ├─ delay完成 -> 内部continuation.resume(Unit)  // 水开了,通知恢复
    │
    ├─ 状态机(state=1)接收结果 -> 执行煮面条
    │
    ├─ 状态机(state=1)-> delay(3000, 状态机自身)  // 挂起3秒,释放线程
    │
    │  (线程继续处理其他任务)
    │
3秒后 ├─ delay完成 -> 状态机.resume(Unit)  // 面条熟了,通知恢复
    │
    ├─ 状态机(state=2)-> 打印"早餐做好了" 
    │
    └─ 状态机 -> 调用方.continuation.resume(Unit)  // 整个任务完成

五、关键总结

  1. suspend 不是阻塞:挂起时会释放线程,让线程去处理其他任务(比如 UI 操作),这是协程高效的核心;
  2. 编译器的作用 :通过 CPS 转换增加Continuation参数,通过状态机处理多个挂起点,自动生成 "暂停 - 恢复" 的逻辑;
  3. Continuation 的角色:保存暂停时的状态(局部变量、执行位置),是 "继续执行的线索";
  4. 状态机的作用 :用state变量记录执行进度,确保恢复时能从正确的位置继续。

简单说,suspend函数就是编译器帮我们实现的 "有记忆的暂停任务",让线程不再傻傻等待,从而提高效率。

相关推荐
用户2018792831673 小时前
厨房里的协程大冒险:launch与async的烹饪之旅
android
木易士心4 小时前
Android Handler 机制原理详解
android·app
用户2018792831674 小时前
CoroutineDispatcher的"自由精灵" - Dispatchers.Unconfined
android
用户2018792831674 小时前
用 “奶茶连锁店的部门分工” 理解各种 CoroutineScope
android
黄额很兰寿4 小时前
深入源码理解LiveData的实现原理
android
黄额很兰寿4 小时前
flow 的冷流和热流 是设么有什么区别?
android
Digitally5 小时前
如何将 Android 联系人备份到 Mac 的 4 种简单
android·macos
2501_915918416 小时前
iOS 混淆与 IPA 加固一页式行动手册(多工具组合实战 源码成品运维闭环)
android·运维·ios·小程序·uni-app·iphone·webview
不吃凉粉14 小时前
Android Studio USB串口通信
android·ide·android studio