状态机

状态机就是:用一个变量记录当前执行到哪个状态,下次再进来时,根据这个状态跳到对应的位置继续执行。

而协程挂起/恢复的本质就是:

挂起时保存状态,恢复时根据状态继续执行。


1. 先看一个普通协程例子

suspend fun test() {
    println("1")
    delay(1000)

    println("2")
    delay(1000)

    println("3")
}

从人的视角看,它是顺序执行:

打印 1
等待 1 秒
打印 2
等待 1 秒
打印 3

但是注意,delay(1000) 会挂起协程。

所以真实执行过程是:

第一次进入 test()
打印 1
遇到 delay,挂起,保存状态:我执行到 delay1 后面了

1 秒后恢复
继续打印 2
遇到 delay,挂起,保存状态:我执行到 delay2 后面了

1 秒后恢复
继续打印 3
结束

这里面就天然有几个状态:

状态 0:刚开始执行
状态 1:第一个 delay 后恢复
状态 2:第二个 delay 后恢复
状态 3:结束

2. 把它改写成状态机形式

原始代码:

suspend fun test() {
    println("1")
    delay(1000)

    println("2")
    delay(1000)

    println("3")
}

可以粗略理解成被编译器改造成类似这样:

class TestStateMachine {

    var state = 0

    fun resume() {
        when (state) {
            0 -> {
                println("1")

                state = 1
                // 启动一个 1 秒后的回调
                delayLike {
                    resume()
                }

                return
            }

            1 -> {
                println("2")

                state = 2
                delayLike {
                    resume()
                }

                return
            }

            2 -> {
                println("3")

                state = 3
                return
            }

            3 -> {
                println("已经结束")
            }
        }
    }

    fun delayLike(callback: () -> Unit) {
        // 假设 1 秒后调用 callback
    }
}

重点在这里:

var state = 0

这个 state 就是当前执行状态。

每次遇到挂起点之前,都会先改状态:

state = 1

然后 return,相当于协程挂起。

等异步操作完成后,再调用:

resume()

这时根据 state 继续执行:

when (state) {
    1 -> {
        println("2")
    }
}

这就是状态机。


3. 状态机核心是什么?

状态机通常有三个核心要素:

1. 状态
2. 事件
3. 状态转移

对应到协程:

状态机概念 协程里的对应
状态 当前执行到哪一步
事件 delay 到期、网络请求返回、回调触发
状态转移 从挂起点恢复,继续执行下一段代码

比如:

println("1")
delay(1000)
println("2")

可以理解为:

状态 0:准备执行 println("1")
事件:执行到 delay
状态转移:state = 1,然后挂起

状态 1:从 delay 后恢复
事件:delay 到期
状态转移:继续执行 println("2")

4. 用图表示协程状态机

对于这个函数:

suspend fun test() {
    println("1")
    delay(1000)

    println("2")
    delay(1000)

    println("3")
}

状态图大概是:

[状态0:开始]
      |
      | 打印 1,遇到 delay
      v
[状态1:等待第一次 delay 完成]
      |
      | delay 完成,恢复
      v
[状态2:打印 2 后等待第二次 delay 完成]
      |
      | delay 完成,恢复
      v
[状态3:打印 3,结束]

更准确一点:

state = 0
   |
   | println("1")
   | delay 挂起前设置 state = 1
   v
state = 1
   |
   | println("2")
   | delay 挂起前设置 state = 2
   v
state = 2
   |
   | println("3")
   v
结束

5. 为什么协程需要状态机?

因为协程会在中间暂停。

普通函数调用是这样的:

fun normal() {
    println("1")
    println("2")
    println("3")
}

它必须一次性执行完。

但是协程函数:

suspend fun test() {
    println("1")
    delay(1000)
    println("2")
}

执行到 delay 时会退出当前执行。

问题来了:

1 秒后怎么知道要从哪里继续?

就靠状态机。

它要记住:

我上次执行到 delay 后面了
下次恢复时要执行 println("2")

所以编译器会生成类似:

when (label) {
    0 -> 从头开始
    1 -> 从第一个挂起点后继续
    2 -> 从第二个挂起点后继续
}

这个 label 就是协程状态机里的状态编号。


6. 更接近 Kotlin 协程底层的写法

Kotlin 的 suspend 函数大概会被编译器转换成带有 Continuation 参数的函数。

你写的是:

suspend fun test() {
    println("1")
    delay(1000)
    println("2")
}

底层概念上类似:

fun test(continuation: Continuation<Unit>): Any {
    // ...
}

Continuation 可以理解成:

恢复协程需要的现场

里面会保存:

1. 当前状态 label
2. 局部变量
3. 上下文 CoroutineContext
4. resumeWith 方法

简化模拟:

class TestContinuation {
    var label = 0

    fun resume() {
        when (label) {
            0 -> {
                println("1")
                label = 1

                delayLike {
                    resume()
                }

                return
            }

            1 -> {
                println("2")
                return
            }
        }
    }
}

这里的:

label

就是状态机里的状态。


7. 有局部变量时,状态机还要保存变量

看这个例子:

suspend fun test() {
    val name = "Tom"

    delay(1000)

    println(name)
}

问题是:

val name = "Tom"

是在 delay 之前创建的,但是 println(name) 在恢复之后才执行。

所以挂起时不只要保存执行位置,还要保存局部变量。

状态机大概会变成:

class TestStateMachine {

    var label = 0
    var name: String? = null

    fun resume() {
        when (label) {
            0 -> {
                name = "Tom"

                label = 1
                delayLike {
                    resume()
                }

                return
            }

            1 -> {
                println(name)
                return
            }
        }
    }
}

也就是说:

跨挂起点还要继续用的局部变量,会被提升成状态机对象里的字段。

这就是你前面说的“保存现场”。


8. 再看一个更完整的例子

原始协程代码:

suspend fun loadUser() {
    println("开始加载")

    val token = getToken()

    println("token = $token")

    val user = getUser(token)

    println("user = $user")
}

假设:

suspend fun getToken(): String
suspend fun getUser(token: String): User

这两个函数都会挂起。

那么状态大概是:

状态 0:开始执行 loadUser
状态 1:getToken 返回后
状态 2:getUser 返回后
状态 3:结束

伪代码:

class LoadUserStateMachine {

    var label = 0

    var token: String? = null
    var user: User? = null

    fun resume(result: Any? = null) {
        when (label) {
            0 -> {
                println("开始加载")

                label = 1
                getTokenAsync { tokenResult ->
                    resume(tokenResult)
                }

                return
            }

            1 -> {
                token = result as String

                println("token = $token")

                label = 2
                getUserAsync(token!!) { userResult ->
                    resume(userResult)
                }

                return
            }

            2 -> {
                user = result as User

                println("user = $user")

                label = 3
                return
            }
        }
    }
}

你可以看到:

label = 1
label = 2

就是在记录执行进度。


9. 状态机可以用在哪里?

状态机不只是协程底层用,业务开发中也很常见。

比如:

1. 订单状态

待支付 -> 已支付 -> 已发货 -> 已完成
       -> 已取消

状态机写法:

enum class OrderState {
    PendingPay,
    Paid,
    Shipped,
    Finished,
    Cancelled
}

状态转移:

fun pay(state: OrderState): OrderState {
    return when (state) {
        OrderState.PendingPay -> OrderState.Paid
        else -> error("当前状态不能支付")
    }
}

2. 播放器状态

Idle -> Playing -> Paused -> Playing -> Stopped
enum class PlayerState {
    Idle,
    Playing,
    Paused,
    Stopped
}
fun onPlay() {
    state = when (state) {
        PlayerState.Idle -> PlayerState.Playing
        PlayerState.Paused -> PlayerState.Playing
        else -> state
    }
}

3. 登录流程

未登录 -> 登录中 -> 登录成功
              -> 登录失败
sealed class LoginState {
    object Idle : LoginState()
    object Loading : LoginState()
    data class Success(val user: User) : LoginState()
    data class Error(val message: String) : LoginState()
}

这个在 Android UI 里非常常见:

when (state) {
    is LoginState.Idle -> showIdle()
    is LoginState.Loading -> showLoading()
    is LoginState.Success -> showUser(state.user)
    is LoginState.Error -> showError(state.message)
}

10. 状态机和协程的关系总结

你可以这样理解:

协程 = 语法层面的顺序写法
状态机 = 编译器底层实现方式

你写:

val token = getToken()
val user = getUser(token)
showUser(user)

看起来是同步顺序代码。

但底层类似:

状态0:调用 getToken,挂起
状态1:getToken 返回,调用 getUser,挂起
状态2:getUser 返回,调用 showUser

所以:

协程让你不用手写状态机,但编译器帮你生成了状态机。


11. 状态机的本质公式

状态机可以抽象成:

当前状态 + 输入事件 = 下一个状态 + 执行动作

比如协程:

当前状态:等待 getToken 返回
输入事件:getToken 成功返回 token
下一个状态:调用 getUser
执行动作:保存 token,发起 getUser 请求

订单:

当前状态:待支付
输入事件:用户支付成功
下一个状态:已支付
执行动作:生成支付记录

播放器:

当前状态:暂停
输入事件:点击播放
下一个状态:播放中
执行动作:继续播放音频

12. 最后一句话理解

结合协程来说,状态机就是:

把一段可能中途暂停的代码拆成多个状态,用一个变量记录当前执行到哪一步。每次恢复时,根据这个状态跳到对应代码继续执行。

协程里的:

suspend
delay
withContext
网络请求挂起

这些挂起点,都会变成状态机里的状态切换点。

所以你可以记住:

协程挂起 = 保存状态 + return
协程恢复 = 根据状态继续执行
状态机 = 管理这些状态和恢复逻辑的结构
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇