前语
在前面文章中,咱们基本对协程的运用以及概念都有了大致了解和运用,可是这还远远不够,关于一些协程的原理,咱们仍是需求把握的,只有这样,咱们才能够知其然知其所以然,碰到协程问题或许报错时,能够看懂源码的报错信息。
本篇文章就拿咱们最了解也最重要的挂起函数来说,关于挂起函数的介绍和运用,能够看文章:# 协程(4) | 挂起函数
在前面文章中,咱们说过挂起函数的实质是Callback
,它能够在挂起点挂起,然后等待一个适宜的机遇恢复,然后完成以同步的代码写出异步的效果。
正文
关于挂起函数的原理,假如要深入研讨直接看源码,十分难以看懂,这儿缺少一个适宜的切入点,所以本篇文章咱们从CPS
转化开端,逐渐剖析其中的原理。
CPS
转化
在前面那篇介绍挂起函数文章中,咱们说过从挂起函数转化为CallBack
方式的过程,叫做CPS
转化(Continuation-Passing-Style Transformation
),当然这儿的Callback
是Continuation
,放个动图来加强回想和了解:
在这个动图中,编译器把suspend
关键字给解析了,在解析前后,咱们重点关注一下函数类型的改变。
仍是suspend
挂起函数时:
/**
* 获取用户信息
* */
suspend fun getUserInfo(): String{
withContext(Dispatchers.IO){
delay(1000)
}
return "Coder"
}
该办法的函数类型是suspend () -> String
,当通过CPS
转化后,变成了(Continuation) -> Any?
类型,这儿参数和回来值都发生了改变,咱们就从这个改变来研讨。
CPS
参数改变
首要便是多了一个Continuation
目标,咱们以下面代码为例:
/**
* 挂起函数,这儿因为获取信息是后边依赖于前面,
* 所以运用挂起函数来解决Callback
* */
suspend fun testCoroutine(){
val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(user,friendList)
println(feedList)
}
/**
* 获取用户信息
* */
suspend fun getUserInfo(): String{
withContext(Dispatchers.IO){
delay(1000)
}
return "Coder"
}
/**
* 获取老友列表
* */
suspend fun getFriendList(user: String): String{
withContext(Dispatchers.IO){
delay(1000)
}
return "Tom,Jack"
}
/**
* 获取和老友的动态列表
* */
suspend fun getFeedList(user: String, list: String): String{
withContext(Dispatchers.IO){
delay(1000)
}
return "[FeedList...]"
}
上面咱们模仿了一个业务场景:先获取用户信息,再获取老友列表,最终获取老友动态列表。上面代码假如以Java的视点来看,testCoroutine
办法的调用状况会是下面这样:
// 改变在这儿
// ↓
fun testCoroutine(continuation: Continuation): Any? {
// 改变在这儿
// ↓
val user = getUserInfo(continuation)
// 改变在这儿
// ↓
val friendList = getFriendList(user, continuation)
// 改变在这儿
// ↓
val feedList = getFeedList(friendList, continuation)
log(feedList)
}
这儿会发现testCoroutine
的参数为continuation
,一起在办法内部调用其他挂起函数时,会把这个coroutine
当做最终一个参数传递给其他挂起函数。
这也就说明了为什么挂起函数需求被另一个挂起函数所调用的原因,假如这儿testCoroutine
是一个一般函数,则它就不会有这个continuation
参数来传递给其他挂起函数。
一起,以咱们之前的了解,挂起函数的实质便是Callback
来看的话,testCoroutine
调用了好几个挂起函数,应该会有好几个匿名内部类实例回调来支撑,可是实际状况是只会有一个continuation
实例被传递,这便是一种十分好的规划。
CPS
回来值改变
接着咱们来看一下回来值的改变:
suspend fun getUserInfo(): String {}
// 改变在这儿
// ↓
fun getUserInfo(cont: Continuation): Any? {}
从这儿咱们发现,本来getUserInfo()
的回来值类型是String
,可是解析完suspend
关键字的函数回来值是Any?
,那本来表明函数回来值类型的String
去哪了呢?
这儿当然不会消失,这儿是换了一种方式存在,这个String
保存在了Continuation<T>
的泛型参数中,即:
suspend fun getUserInfo(): String {}
// 改变在这儿
// ↓
fun getUserInfo(cont: Continuation<String>): Any? {}
知道这个改变后,那回来类型Any?
代表什么意思呢?
这儿可得留意了,通过CPS
转化后的办法的回来值,有一个重要的效果,便是:标志该挂起函数有没有被挂起。
这儿听着有点古怪,挂起函数不便是要挂起吗 ?其实不然,当挂起函数内没有调用其他挂起函数或许完成挂起函数时,它就不需求挂起。
比方下面代码:
/**
* 获取用户信息
* */
suspend fun getUserInfo(): String{
withContext(Dispatchers.IO){
delay(1000)
}
return "Coder"
}
在getUserInfo
办法中,当履行到withContext
时,该办法就会被挂起,这时就会回来CoroutineSingletons.COROUTINE_SUSPENDED
,用来标志getUserInfo
被挂起了。
可是比方下面代码:
/**
* 函数内部没有挂起
* */
suspend fun noSuspendGetUserInfo(): String{
return "zyh"
}
在这种状况下,该函数就和一般函数相同,不会被挂起,一起IDE会提示你这个suspend
关键字是多余的。可是,IDE遇到suspend
关键字时,就会发生CPS
转化,所以上面办法通过CPS
转化后,回来类型是no suspend
的String
类型,这也是一种伪挂起。
所以这儿咱们也就了解了为什么CPS
后的挂起函数回来值类型是Any?
了。
挂起函数CPS
后的履行流程
了解了挂起函数在通过CPS
转化后的类型改变,以及参数和回来值类型所代表的意义,使用这种特性,就能够规划出咱们熟知的挂起函数了。
首要,这儿不会直接给出反编译后的代码,而是给出大致等价的代码,改进了可读性。其次,先通过这些简化代码学习原理,最终再通过反编译来验证咱们的主意。
咱们直接以下面代码为例:
/**
* 挂起函数,这儿因为获取信息是后边依赖于前面,
* 所以运用挂起函数来解决Callback
* */
suspend fun testCoroutine(){
val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(user,friendList)
println(feedList)
}
这个办法比较复杂,触及到了3个挂起函数调用。在通过CPS
转化后,首要在testCoroutine
办法中会多出一个ContinuationImpl
的子类,它是原理完成的中心,大致代码如下:
/**
* CPS后的等价代码
* @param completion [Continuation]类型的参数
* */
fun testCoroutine(completion: Continuation<Any?>): Any? {
/**
* 实质是匿名内部类,这儿给取了一个姓名[TestContinuation], 结构
* 参数仍是传递一个[Continuation]类型的字段
* */
class TestContinuation(completion: Continuation<Any?>) : ContinuationImpl(completion){
//表明状况机的状况
var label: Int = 0
//当时挂起函数履行的成果
var result: Any? = null
//用于保存挂起计算的成果,中心值
var mUser: Any? = null
var mFriendList: Any? = null
/**
* 状况机进口,该类是ContinuationImpl中的抽象办法,一起ContinuationImpl
* 又是承继至[Continuation],所以[Continuation]中的resumeWith办法会回调
* 该invokeSuspend办法。
*
* 即每当调用挂起函数回来时,该办法都会被调用,在办法内部,先通过result记录
* 挂起函数的履行成果,再切换labeal,最终再调用testCoroutine办法
* */
override fun invokeSuspend(_result: Result<Any?>): Any?{
result = _result
label = label or Int.Companion.MIN_VALUE
return testCoroutine(this)
}
}
}
在办法中,会生成一个匿名内部类,这儿为了阅读便利,咱们起了一个姓名为TestContinuation
,它是Continuation
的子类,里边各种字段的效果能够直接检查注释,里边的中心办法便是invokeSuspend
办法,它是进入状况机的进口。
然后,就要判别testCoroutine
办法是否是第一次运行:
/**
* CPS后的等价代码
* @param completion [Continuation]类型的参数
* */
fun testCoroutine(completion: Continuation<Any?>): Any? {
...
val continuation = if (completion is TestContinuation){
completion
}else{
//作为参数
TestContinuation(completion)
}
}
在这儿咱们发现,当testCoroutine
是第一次调用时,运用前面的匿名内部类创立实例,而且把参数传递进去;当不是第一次调用时,就能够复用同一个continuation
,这也就验证了它不像Callback
那样,需求创立多个接口实例。
接着是几个变量:
//3个变量,对应原函数的3个变量
lateinit var user: String
lateinit var friendList: String
lateinit var feedList: String
//result接收挂起函数的运行成果
var result = continuation.result
//suspendReturn表明乖巧函数的回来值
var suspendReturn: Any? = null
//该flag表明当时函数被挂起了
val sFlag = CoroutineSingles.CORTOUINE_SUSPEND
分别表明原函数中暂时变量、挂起函数履行的成果,以及是否被挂起的标志。
这儿需求记住前面的常识,即当一个挂起函数是真的挂起函数时,它会回来sFlag
,这个在判别流程中十分重要。
在本来的testCoroutine
办法中,咱们调用了3次挂起函数,记住咱们挂起函数的实质是挂起什么来着?挂起的是后边履行的代码,所以在CPS
后,状况机逻辑会被分为4个部分:
when(continuation.label){
0 -> {
//检测反常
throwOnFailure(result)
//将label设置为1,预备进入下一个状况
continuation.label = 1
//履行getUserInfo
suspendReturn = getUserInfo(continuation)
//判别是否挂起
if (suspendReturn == sFlag){
return suspendReturn
}else{
result = suspendReturn
}
}
1 -> {
throwOnFailure(result)
// 获取 user 值
user = result as String
// 将协程成果存到 continuation 里
continuation.mUser = user
// 预备进入下一个状况
continuation.label = 2
// 履行 getFriendList
suspendReturn = getFriendList(user, continuation)
// 判别是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
}
}
2 -> {
throwOnFailure(result)
user = continuation.mUser as String
//获取friendList的值
friendList = result as String
//将挂起函数成果保存到continuation中
continuation.mUser = user
continuation.mFriendList = friendList
//预备进入下一个阶段
continuation.label = 3
//履行获取feedList
suspendReturn = getFeedList(user,friendList,continuation)
//判别是否挂起
if (suspendReturn == sFlag){
return suspendReturn
}else{
result = suspendReturn
}
}
3 -> {
throwOnFailure(result)
user = continuation.mUser as String
friendList = continuation.mFriendList as String
feedList = continuation.result as String
loop = false
}
}
这部分代码了解起来有点困难,咱们来一步一步剖析走一遍:
- 第一次调用
testCoroutine
时,会给continuation
赋值,为TestContinuation
类型,默许状况下continuaiton
中的label
为0,所以会进入when
的0分支。 - 在
when
的0分支中,会先检查反常,然后将continuaiton
的label
设置为1,然后履行getUserInfo
办法。因为该办法是真挂起函数,所以回来值suspendReturn
和sFlag
相同,这时testCoroutine
会直接被return
,即完成运行。 - 在
getUserInfo
办法中,会模仿网络恳求,当获取到网络恳求数据后,会调用getUserInfo
的continuation
参数的invokeSuspend(user)
办法,留意该办法的continuation
和前一次调用testCoroutine
的continuation
是同一个。 - 依据
TestContinuation
的定义,这时该continuation
实例中的result
便是获取到的user
信息,然后label
为1,然后开端第2次调用testCoroutine
办法,一起参数依旧是这个continuation
。 - 第2次调用
testCoroutine
时,continuaiton
不会再被创立,这时办法的result
变量会保存user
,会进入when
的1分支里边。 - 在1分支里,办法的
user
变量为获取到的用户信息的String类型,然后将该成果保存到continuation
的mUser
变量中。这时,咱们getUserInfo
办法的成果值就保存到了仅有continuation
中,接着label
设置为2,调用getFriendList
办法,同样的该办法是挂起函数,这时第2次调用的testCoroutine
办法被return
掉。 - 在
getFriendList
办法中,相同的逻辑,当获取到老友列表后,会回调仅有continuaiton
的invokeSuspend(friendList)
办法,这时result
为老友列表信息,一起开启第三次调用testCoroutine
办法。 - 第三次调用
testCoroutine
时,会进入2分支,在该分支中,会给仅有continuation
的mFriendList
赋值老友列表信息,然后label
设置为3,调用getFeedList
挂起函数,这时第三次testCoroutine
被return
。 - 在
getFeedList
中,回调仅有continuation
的invokeSuspend(feedList)
办法,这时result
保存的是是动态信息,一起开端调用第四次testCoroutine
办法。 - 在第四次调用
testCoroutine
办法中,会进入3分支,在这儿咱们能够获取用户信息(保存在continuation
的mUser
字段中)、老友列表信息(保存在continuation
的mFriendList
字段中)和动态信息(保存在result
)字段中。
到这儿,一个调用3个挂起函数的挂起函数就剖析完了,不得不说,规划的十分巧妙。使用仅有的continuation
来保存挂起函数履行的值,通过多次调用函数自己来分割开来挂起函数。
伪挂起函数CPS
后的履行流程
上面流程假如仔细思考的话,仍是有个问题,那便是判别是否是挂起函数,假如是挂起函数咱们在continuation
中调用invokeSuspend()
办法能够再次进入testCoroutine()
函数,可是不是挂起函数时,这个怎么进入呢?
其实当不是挂起函数时,并不会再次调用testCoroutine()
函数,而是会直接进入when
句子代码,这儿其实便是使用goto
句子,而goto
句子在Kotlin现已没有了,所以在Java中类似下面代码来完成跳转:
...
label: whenStart
when (continuation.label) {
0 -> {
...
}
1 -> {
...
suspendReturn = noSuspendFriendList(user, continuation)
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
// 让程序跳转到 label 符号的地方
// 然后再履行一次 when 表达式
goto: whenStart
}
}
2 -> {
...
}
3 -> {
...
}
}
所以在反编译后的代码也会有许多label
,便是为了完成这种跳转。
总结
本篇文章介绍了挂起函数的原理,其实通过CPS
转化后,能够发现实质便是一个状况机。通过将调用不同挂起函数的状况,转变为状况机中的不同分支,办法本身不保存挂起函数的成果,而是通过仅有的一个continuation
实例来保存。
通过多次调用办法,进入不同的分支,关于伪挂起函数,进行直接goto
跳转到状况机。
这篇文章学习后,我相信你肯定会有下面疑惑:
- 为什么挂起函数回来的是固定值,怎么完成一个挂起函数。
-
continuation
这个变量是从哪来的,就比方本例中的testCoroutine
通过CPS
转化后的参数是哪来的,有什么特别之处。
这些问题,咱们后边继续探究。