前语

在前面文章中,咱们基本对协程的运用以及概念都有了大致了解和运用,可是这还远远不够,关于一些协程的原理,咱们仍是需求把握的,只有这样,咱们才能够知其然知其所以然,碰到协程问题或许报错时,能够看懂源码的报错信息。

本篇文章就拿咱们最了解也最重要的挂起函数来说,关于挂起函数的介绍和运用,能够看文章:# 协程(4) | 挂起函数

在前面文章中,咱们说过挂起函数的实质是Callback,它能够在挂起点挂起,然后等待一个适宜的机遇恢复,然后完成以同步的代码写出异步的效果。

正文

关于挂起函数的原理,假如要深入研讨直接看源码,十分难以看懂,这儿缺少一个适宜的切入点,所以本篇文章咱们从CPS转化开端,逐渐剖析其中的原理。

CPS转化

在前面那篇介绍挂起函数文章中,咱们说过从挂起函数转化为CallBack方式的过程,叫做CPS转化(Continuation-Passing-Style Transformation),当然这儿的CallbackContinuation,放个动图来加强回想和了解:

fcf5b8eead81466bb7eacc017f0c1377_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp

在这个动图中,编译器把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 suspendString类型,这也是一种伪挂起。

所以这儿咱们也就了解了为什么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
    }
}

这部分代码了解起来有点困难,咱们来一步一步剖析走一遍:

  1. 第一次调用testCoroutine时,会给continuation赋值,为TestContinuation类型,默许状况下continuaiton中的label为0,所以会进入when的0分支。
  2. when的0分支中,会先检查反常,然后将continuaitonlabel设置为1,然后履行getUserInfo办法。因为该办法是真挂起函数,所以回来值suspendReturnsFlag相同,这时testCoroutine会直接被return,即完成运行。
  3. getUserInfo办法中,会模仿网络恳求,当获取到网络恳求数据后,会调用getUserInfocontinuation参数的invokeSuspend(user)办法,留意该办法的continuation和前一次调用testCoroutinecontinuation是同一个。
  4. 依据TestContinuation的定义,这时该continuation实例中的result便是获取到的user信息,然后label为1,然后开端第2次调用testCoroutine办法,一起参数依旧是这个continuation
  5. 第2次调用testCoroutine时,continuaiton不会再被创立,这时办法的result变量会保存user,会进入when的1分支里边。
  6. 在1分支里,办法的user变量为获取到的用户信息的String类型,然后将该成果保存到continuationmUser变量中。这时,咱们getUserInfo办法的成果值就保存到了仅有continuation中,接着label设置为2,调用getFriendList办法,同样的该办法是挂起函数,这时第2次调用的testCoroutine办法被return掉。
  7. getFriendList办法中,相同的逻辑,当获取到老友列表后,会回调仅有continuaitoninvokeSuspend(friendList)办法,这时result为老友列表信息,一起开启第三次调用testCoroutine办法。
  8. 第三次调用testCoroutine时,会进入2分支,在该分支中,会给仅有continuationmFriendList赋值老友列表信息,然后label设置为3,调用getFeedList挂起函数,这时第三次testCoroutinereturn
  9. getFeedList中,回调仅有continuationinvokeSuspend(feedList)办法,这时result保存的是是动态信息,一起开端调用第四次testCoroutine办法。
  10. 在第四次调用testCoroutine办法中,会进入3分支,在这儿咱们能够获取用户信息(保存在continuationmUser字段中)、老友列表信息(保存在continuationmFriendList字段中)和动态信息(保存在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转化后的参数是哪来的,有什么特别之处。

这些问题,咱们后边继续探究。