当前位置:   article > 正文

Kotlin Jetpack 实战 _ 09,40道安卓面试_jetpack suspend 函数

jetpack suspend 函数

这听起来有点绕:挂起函数,就是可以被挂起的函数,它还能不被挂起吗?是的,挂起函数也能不被挂起。

让我们来理清几个概念:

只要有 suspend 修饰的函数,它就是挂起函数,比如我们前面的例子:

suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return “BoyCoder”
}

当 getUserInfo() 执行到 withContext的时候,就会返回 CoroutineSingletons.COROUTINE_SUSPENDED 表示函数被挂起了。

现在问题来了,请问下面这个函数是挂起函数吗:

// suspend 修饰
// ↓
suspend fun noSuspendFriendList(user: String): String{
// 函数体跟普通函数一样
return “Tom, Jack”
}

答案:它是挂起函数。但它跟一般的挂起函数有个区别:它在执行的时候,并不会被挂起,因为它就是普通函数。当你写出这样的代码后,IDE 也会提示你,suspend 是多余的:

noSuspendFriendList() 被调用的时候,不会挂起,它会直接返回 String 类型:"no suspend"。这样的挂起函数,你可以把它看作伪挂起函数

返回类型是 Any?的原因

由于 suspend 修饰的函数,既可能返回 CoroutineSingletons.COROUTINE_SUSPENDED,也可能返回实际结果"no suspend",甚至可能返回 null,为了适配所有的可能性,CPS 转换后的函数返回值类型就只能是 Any?了。

小结

  • suspend 修饰的函数就是挂起函数
  • 挂起函数,在执行的时候并不一定都会挂起
  • 挂起函数只能在其他挂起函数中被调用
  • 挂起函数里包含其他挂起函数的时候,它才会真正被挂起

以上就是 CPS 转换过程中,函数签名的细节。

然而,这并不是 CPS 转换的全部,因为我们还不知道 Continuation 到底是什么。

6. CPS 转换

Continuation 这个单词,如果你查词典维基百科,可能会一头雾水,太抽象了。

通过我们文章的例子来理解 Continuation,会更容易一些。

首先,我们只需要把握住 Continuation 的词源 Continue 即可。Continue 是继续的意思,Continuation 则是继续下去要做的事情,接下来要做的事情

放到程序中,Continuation 则代表了,程序继续运行下去需要执行的代码,接下来要执行的代码 或者 剩下的代码

以上面的代码为例,当程序运行 getUserInfo() 的时候,它的 Continuation则是下图红框的代码:

Continuation 就是接下来要运行的代码剩余未执行的代码

理解了 Continuation,以后,CPS就容易理解了,它其实就是:将程序接下来要执行的代码进行传递的一种模式。

CPS 转换,就是将原本的同步挂起函数转换成CallBack 异步代码的过程。这个转换是编译器在背后做的,我们程序员对此无感知。

也许会有小伙伴嗤之以鼻:这么简单粗暴?三个挂起函数最终变成三个 Callback 吗?

当然不是。思想仍然是 CPS 的思想,但要比 Callback 高明很多。

接下来,我们一起看看挂起函数反编译后的代码是什么样吧。前面铺垫了这么多,全都是为了下一个部分准备的。

7. 字节码反编译

字节码反编译成 Java 这种事,我们干过很多次了。跟往常不同的是,这次我不会直接贴反编译后的代码,因为如果我直接贴出反编译后的 Java 代码,估计会吓退一大波人。协程反编译后的代码,逻辑实在是太绕了,可读性实在太差了。这样的代码,CPU 可能喜欢,但真不是人看的。

所以,为了方便大家理解,接下来我贴出的代码是我用 Kotlin 翻译后大致等价的代码,改善了可读性,抹掉了不必要的细节。如果你能把这篇文章里所有内容都弄懂,你对协程的理解也已经超过大部分人了。

进入正题,这是我们即将研究的对象,testCoroutine 反编译前的代码:

suspend fun testCoroutine() {
log(“start”)
val user = getUserInfo()
log(user)
val friendList = getFriendList(user)
log(friendList)
val feedList = getFeedList(friendList)
log(feedList)
}

反编译后,testCoroutine函数的签名变成了这样:

// 没了 suspend,多了 completion
fun testCoroutine(completion: Continuation<Any?>): Any? {}

由于其他几个函数也是挂起函数,所以它们的函数签名也会改变:

fun getUserInfo(completion: Continuation<Any?>): Any?{}
fun getFriendList(user: String, completion: Continuation<Any?>): Any?{}
fun getFeedList(friendList: String, completion: Continuation<Any?>): Any?{}

接下来我们分析 testCoroutine() 的函数体,因为它相当复杂,涉及到三个挂起函数的调用。

首先,在 testCoroutine 函数里,会多出一个 ContinuationImpl 的子类,它的是整个协程挂起函数的核心。代码里的注释非常详细,请仔细看。

fun testCoroutine(completion: Continuation<Any?>): Any? {

class TestContinuation(completion: Continuation<Any?>?) : ContinuationImpl(completion) {
// 表示协程状态机当前的状态
var label: Int = 0
// 协程返回结果
var result: Any? = null

// 用于保存之前协程的计算结果
var mUser: Any? = null
var mFriendList: Any? = null

// invokeSuspend 是协程的关键
// 它最终会调用 testCoroutine(this) 开启协程状态机
// 状态机相关代码就是后面的 when 语句
// 协程的本质,可以说就是 CPS + 状态机
override fun invokeSuspend(_result: Result<Any?>): Any? {
result = _result
label = label or Int.Companion.MIN_VALUE
return testCoroutine(this)
}
}
}

接下来则是要判断 testCoroutine 是不是初次运行,如果是初次运行,就要创建一个 TestContinuation 的实例对象。

// ↓
fun testCoroutine(completion: Continuation<Any?>): Any? {

val continuation = if (completion is TestContinuation) {
completion
} else {
// 作为参数
// ↓
TestContinuation(completion)
}
}

  • invokeSuspend 最终会调用 testCoroutine,然后走到这个判断语句
  • 如果是初次运行,会创建一个 TestContinuation 对象,completion 作为了参数
  • 这相当于用一个新的 Continuation 包装了旧的 Continuation
  • 如果不是初次运行,直接将 completion 赋值给 continuation
  • 这说明 continuation 在整个运行期间,只会产生一个实例,这能极大的节省内存开销(对比 CallBack)

接下来是几个变量的定义,代码里会有详细的注释:

// 三个变量,对应原函数的三个变量
lateinit var user: String
lateinit var friendList: String
lateinit var feedList: String

// result 接收协程的运行结果
var result = continuation.result

// suspendReturn 接收挂起函数的返回值
var suspendReturn: Any? = null

// CoroutineSingletons 是个枚举类
// COROUTINE_SUSPENDE
D 代表当前函数被挂起了
val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED

然后就到了我们的状态机的核心逻辑了,具体看注释吧:

when (continuation.label) {
0 -> {
// 检测异常
throwOnFailure(result)

log(“start”)
// 将 label 置为 1,准备进入下一次状态
continuation.label = 1

// 执行 getUserInfo
suspendReturn = getUserInfo(continuation)

// 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}

1 -> {
throwOnFailure(result)

// 获取 user 值
user = result as String
log(user)
// 将协程结果存到 continuation 里
continuation.mUser = user
// 准备进入下一个状态
continuation.label = 2

// 执行 getFriendList
suspendReturn = getFriendList(user, continuation)

// 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}

2 -> {
throwOnFailure(result)

user = continuation.mUser as String

// 获取 friendList 的值
friendList = result as String
log(friendList)

// 将协程结果存到 continuation 里
continuation.mUser = user
continuation.mFriendList = friendList

// 准备进入下一个状态
continuation.label = 3

// 执行 getFeedList
suspendReturn = getFeedList(friendList, continuation)

// 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}

3 -> {
throwOnFailure(result)

user = continuation.mUser as String
friendList = continuation.mFriendList as String
feedList = continuation.result as String
log(feedList)
loop = false
}
}

  • when 表达式实现了协程状态机
  • continuation.label 是状态流转的关键
  • continuation.label 改变一次,就代表协程切换了一次
  • 每次协程切换后,都会检查是否发生异常
  • testCoroutine 里的原本的代码,被拆分到状态机里各个状态中,分开执行
  • getUserInfo(continuation),getFriendList(user, continuation),getFeedList(friendList, continuation) 三个函数调用传的同一个 continuation 实例。
  • 一个函数如果被挂起了,它的返回值会是:CoroutineSingletons.COROUTINE_SUSPENDED
  • 切换协程之前,状态机会把之前的结果以成员变量的方式保存在 continuation 中。

警告:以上的代码是我用 Kotlin 写出的改良版反编译代码,协程反编译后真实的代码后面我也会放出来,请继续看。

8. 协程状态机动画演示

上面一大串文字和代码看着是不是有点晕?请看看这个动画演示,看完动画演示了,回过头再看上面的文字,你会有更多收获。

是不是完了呢?并不,因为上面的动画仅演示了每个协程正常挂起的情况。 如果协程并没有真正挂起呢?协程状态机会怎么运行?

协程未挂起的情况

要验证也很简单,我们将其中一个挂起函数改成伪挂起函数即可。

// “伪”挂起函数
// 虽然它有 suspend 修饰,但执行的时候并不会真正挂起,因为它函数体里没有其他挂起函数
// ↓
suspend fun noSuspendFriendList(user: String): String{
return “Tom, Jack”
}

suspend fun testNoSuspend() {
log(“start”)
val user = getUserInfo()
log(user)
// 变化在这里
// ↓
val friendList = noSuspendFriendList(user)
log(friendList)
val feedList = getFeedList(friendList)
log(feedList)
}

testNoSuspend()这样的一个函数体,它的反编译后的代码逻辑怎么样的?

答案其实很简单,它的结构跟前面的testCoroutine()是一致的,只是函数名字变了而已,Kotlin 编译器 CPS 转换的逻辑只认 suspend 关键字。就算是“伪”挂起函数,Kotlin 编译器也照样会进行 CPS 转换。

when (continuation.label) {
0 -> {

}

1 -> {

// 变化在这里
// ↓
suspendReturn = noSuspendFriendList(user, continuation)

// 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}

2 -> {

}

3 -> {

}
}

testNoSuspend()的协程状态机是怎么运行的?

其实很容易能想到,continuation.label = 0,2,3 的情况都是不变的,唯独在 label = 1 的时候,suspendReturn == sFlag这里会有区别。

具体区别我们通过动画来看吧:

通过动画我们很清楚的看到了,对于“伪”挂起函数suspendReturn == sFlag是会走 else 分支的,在 else 分支里,协程状态机会直接进入下一个状态。

现在只剩最后一个问题了:

if (suspendReturn == sFlag) {
} else {
// 具体代码是如何实现的?
// ↓
//go to next state
}

答案其实也很简单:如果你去看协程状态机的字节码反编译后的 Java,会看到很多 label。协程状态机底层字节码是通过 label 来实现这个go to next state的。由于 Kotlin 没有类似 goto 的语法,下面我用伪代码来表示go to next state的逻辑。

// 伪代码
// Kotlin 没有这样的语法
// ↓ ↓
label: whenStart
when (continuation.label) { <

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/259955
推荐阅读
相关标签
  

闽ICP备14008679号