Kotlin Vocabulary | 揭秘协程中的 suspend 修饰符
📚 如果您是 Android 平台上协程的初学者,请查阅下面这些协程 codelab:
在 Android 应用中使用协程 https://codelabs.developers.google.com/codelabs/kotlin-coroutines/#0 协程的进阶使用: Kotlin Flow 和 Live Data https://codelabs.developers.google.com/codelabs/advanced-kotlin-coroutines/#0
协程 101
协程简化了 Android 平台的异步操作。正如官方文档《利用 Kotlin 协程提升应用性能》所介绍的,我们可以使用协程管理那些以往可能阻塞主线程或者让应用卡死的异步任务。
《利用 Kotlin 协程提升应用性能》
https://developer.android.google.cn/kotlin/coroutines
协程也可以帮我们用命令式代码替换那些基于回调的 API。例如,下面这段使用了回调的异步代码:
// 简化的只考虑了基础功能的代码
fun loginUser(userId: String, password: String, userResult: Callback<User>) {
// 异步回调
userRemoteDataSource.logUserIn { user ->
// 成功的网络请求
userLocalDataSource.logUserIn(user) { userDb ->
// 保存结果到数据库
userResult.success(userDb)
}
}
}
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
Suspend 的工作原理
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
// UserRemoteDataSource.kt
suspend fun logUserIn(userId: String, password: String): User
// UserLocalDataSource.kt
suspend fun logUserIn(userId: String): UserDb
有限状态机
https://en.wikipedia.org/wiki/Finite-state_machine
Continuation 接口
挂起函数通过 Continuation 对象在方法间互相通信。Continuation 其实只是一个具有泛型参数和一些额外信息的回调接口,稍后我们会看到,它会实例化挂起函数所生成的状态机。
Continuation
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-continuation/index.html
Continuation
https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/coroutines/Continuation.kt
我们先来看看它的声明:
interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(value: Result<T>)
}
context 是 Continuation 将会使用的 CoroutineContext;
resumeWith 会恢复协程的执行,同时传入一个 Result 参数,Result 中会包含导致挂起的计算结果或者是一个异常。
Result
https://github.com/Kotlin/kotlinx.coroutines/blob/master/stdlib-stubs/src/Result.kt
注意: 从 Kotlin 1.3 开始,您也可以使用 resumeWith 对应的扩展函数: resume (value: T) 和 resumeWithException (exception: Throwable)。
resume
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/resume.html
resumeWithException
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/resume-with-exception.html
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
completion.resume(userDb)
}
当使用 suspendCoroutine 或 suspendCancellableCoroutine (首选使用) 来将基于回调的 API 转化为协程时,会直接与一个 Continuation 对象进行交互。它会用于恢复那些执行了参数代码块后挂起的协程; 您可以在一个挂起函数上使用 startCoroutine 扩展函数,它会接收一个 Continuation 对象作为参数,并会在新的协程结束时调用它,无论其运行结果是成功还是异常。
suspendCoroutine https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/suspend-coroutine.html suspendCancellableCoroutine https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html startCoroutine https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/start-coroutine.html
使用不同的 Dispatcher
DispatchedContinuation https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt
生成状态机
特殊说明: 本文接下来所展示的,并不是与编译器生成的字节码完全相同的代码,而是足够精确的,能够确保您理解其内部发生了什么的 Kotlin 代码。这些声明由版本为 1.3.3 的协程库生成,可能会在其未来的版本中作出修改。
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
// Label 0 -> 第一次执行
val user = userRemoteDataSource.logUserIn(userId, password)
// Label 1 -> 从 userRemoteDataSource 恢复
val userDb = userLocalDataSource.logUserIn(user)
// Label 2 -> 从 userLocalDataSource 恢复
completion.resume(userDb)
}
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
when(label) {
// Label 0 -> 第一次执行
userRemoteDataSource.logUserIn(userId, password)
}
// Label 1 -> 从 userRemoteDataSource 恢复
userLocalDataSource.logUserIn(user)
}
// Label 2 -> 从 userLocalDataSource 恢复
completion.resume(userDb)
}
else -> throw IllegalStateException(...)
}
}
接下来,编译器会创建一个私有类,它会:
保存必要的数据;
递归调用 loginUser 函数来恢复执行。
您可以查看下面提供的编译器生成类的近似版本。
特别说明: 注释不是由编译器生成的,而是由作者添加的。添加它们是为了解释这些代码的作用,也能让后面的代码更加容易理解。
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
class LoginUserStateMachine(
// completion 参数是调用了 loginUser 的函数的回调
completion: Continuation<Any?>
): CoroutineImpl(completion) {
// suspend 的本地变量
var user: User? = null
var userDb: UserDb? = null
// 所有 CoroutineImpls 都包含的通用对象
var result: Any? = null
var label: Int = 0
// 这个方法再一次调用了 loginUser 来切换
// 状态机 (标签会已经处于下一个状态)
// result 将会是前一个状态的计算结果
override fun invokeSuspend(result: Any?) {
this.result = result
loginUser(null, null, this)
}
}
...
}
函数是第一次被调用; 函数已经从前一个状态中恢复。
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
...
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
...
}
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
...
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
when(continuation.label) {
0 -> {
// 错误检查
throwOnFailure(continuation.result)
// 下次 continuation 被调用时, 它应当直接去到状态 1
continuation.label = 1
// Continuation 对象被传入 logUserIn 函数,从而可以在结束时恢复
// 当前状态机的执行
userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
}
1 -> {
// 检查错误
throwOnFailure(continuation.result)
// 获得前一个状态的结果
continuation.user = continuation.result as User
// 下次这 continuation 被调用时, 它应当直接去到状态 2
continuation.label = 2
// Continuation 对象被传入 logUserIn 函数,从而可以在结束时恢复
// 当前状态机的执行
userLocalDataSource.logUserIn(continuation.user, continuation)
}
... // 故意遗漏了最后一个状态
}
}
花一些时间浏览上面的代码,看看您是否能注意到与之前代码之间的差异。下面我们来看看编译器生成了什么:
when 语句的参数是 LoginUserStateMachine 实例内的 label; 每一次处理新的状态时,为了防止函数被挂起时运行失败,都会进行一次检查; 在调用下一个挂起函数 (即 logUserIn) 前,LoginUserStateMachine 的 label 都会更新到下一个状态; 在当前的状态机中调用另一个挂起函数时,continuation 的实例 (LoginUserStateMachine 类型) 会被作为参数传递过去。而即将被调用的挂起函数也同样被编译器转换成一个相似的状态机,并且接收一个 continuation 对象作为参数。当被调用的挂起函数的状态机运行结束时,它将恢复当前状态机的执行。
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
...
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
when(continuation.label) {
...
2 -> {
// 错误检查
throwOnFailure(continuation.result)
// 获取前一个状态的结果
continuation.userDb = continuation.result as UserDb
// 恢复调用了当前函数的函数的执行
continuation.cont.resume(continuation.userDb)
}
else -> throw IllegalStateException(...)
}
}
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
class LoginUserStateMachine(
// completion 参数是调用了 loginUser 的函数的回调
completion: Continuation<Any?>
): CoroutineImpl(completion) {
// 要在整个挂起函数中存储的对象
var user: User? = null
var userDb: UserDb? = null
// 所有 CoroutineImpls 都包含的通用对象
var result: Any? = null
var label: Int = 0
// 这个函数再一次调用了 loginUser 来切换
// 状态机 (标签会已经处于下一个状态)
// result 将会是前一个状态的计算结果
override fun invokeSuspend(result: Any?) {
this.result = result
loginUser(null, null, this)
}
}
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
when(continuation.label) {
0 -> {
// 错误检查
throwOnFailure(continuation.result)
// 下次 continuation 被调用时, 它应当直接去到状态 1
continuation.label = 1
// Continuation 对象被传入 logUserIn 函数,从而可以在结束时恢复
// 当前状态机的执行
userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
}
1 -> {
// 检查错误
throwOnFailure(continuation.result)
// 获得前一个状态的结果
continuation.user = continuation.result as User
// 下次这 continuation 被调用时, 它应当直接去到状态 2
continuation.label = 2
// Continuation 对象被传入 logUserIn 方法,从而可以在结束时恢复
// 当前状态机的执行
userLocalDataSource.logUserIn(continuation.user, continuation)
}
2 -> {
// 错误检查
throwOnFailure(continuation.result)
// 获取前一个状态的结果
continuation.userDb = continuation.result as UserDb
// 恢复调用了当前函数的执行
continuation.cont.resume(continuation.userDb)
}
else -> throw IllegalStateException(...)
}
}
推荐阅读