Kotlin 挂起函数CPS转换原理解析
作者:无糖可乐爱好者
正文
普通函数加上suspend
之后就成为了一个挂起函数,Kotlin编译器会将这个挂起函数转换成了带有参数Continuation<T>
的一个普通函数,Continuation
是一个接口,它跟Java中的Callback
有着一样的功能,这个转换过程被称为CPS转换。
1.什么是CPS转换
挂起函数中的CPS转换就是把挂起函数转换成一个带有Callback
的函数,这里的 Callback
就是 Continuation
接口。在这个过程中会发生函数参数的变化和函数返回值的变化。
suspend fun getAreaCode(): String { delay(1000L) return "100011" } //函数参数的变化 suspend ()变成了(Continuation) //函数返回值的变化 -> String变成了 ->Any? //变化后的代码如下 private fun getProvinceCode(c: Continuation<String>): Any? { return "100000" }
2.CPS的过程是怎么让参数改变的
这个问题的答案其实在挂起函数哪里提到过,Kotlin代码可以运行主要是Kotlin编译器将代码转换成了Java字节码,然后交给Java虚拟机执行,那么转换成Java后的挂起函数就是一个带有Callback
回调的普通函数,对应Kotlin的话就是Continuation
函数,那么这是参数的改变,代码的转换就是:
private suspend fun getProvinceCode(): String { delay(1000L) return "100000" } /** * Kotlin转换的Java代码 */ private static final Object getProvinceCode(Continuation $completion) { return "100000"; } private fun getProvinceCode(c: Continuation<String>): Any? { return "100000" }
这里就可以解答一个疑问:为什么普通函数不可以调用挂起函数了? 这是因为挂起函数被Kotlin编译器便后默认是需要传入一个Continuation
参数的,而普通函数没有这个类型的参数。
3.CPS的过程是怎么让返回值改变的
原本的代码是返回了一个String类型的值,但是通过CPS转换后String变成了Any?,如果说String是Any?的子类这样也行的通,但是String为什么没了呢,以及为什么会多了一个Any?
首先解释这个String为什么没有了,其实String不是没有了,而是换了个地方
// 换到了这里 private fun getProvinceCode(c: Continuation<String>): Any? { return "100000" }
CPS转换它必定是一个等价交换, 否则编译后的程序就失去了原本的作用,也就是说这个String它会以另一种形式存在。
现在解释第二个问题,为什么会多了一个Any?
挂起函数经过 CPS 转换后,它的返回值有一个重要作用:标志该挂起函数有没有被挂起。 挂起函数也有可能不会被挂起,上面的挂起函数中都添加了delay(1000L)
,而delay(1000L)
是一个挂起函数这个是已经知道的,那么如果不加它会怎么样呢
上面的函数删除了delay(1000L)
只有suspend
成了灰色并且提示信息:suspend是多余的, 用两段代码做个对比
//有效的挂起函数 private suspend fun suspendFun(): String { delay(1000L) return "100000" } //无效的挂起函数 private suspend fun noSuspendFun(): String { return "100000" }
反编译后的Java代码
//函数调用 @Nullable public static final Object main(@NotNull Continuation $completion) { Object var10000 = suspendFun($completion); return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE; } // $FF: synthetic method public static void main(String[] var0) { RunSuspendKt.runSuspend(new SuspendDemoKt$$$main(var0)); } //有效的挂起函数 private static final Object suspendFun(Continuation var0) { Object $continuation; label20: { if (var0 instanceof <undefinedtype>) { $continuation = (<undefinedtype>)var0; if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) { ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE; break label20; } } $continuation = new ContinuationImpl(var0) { // $FF: synthetic field Object result; int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return SuspendDemoKt.suspendFun(this); } }; } Object $result = ((<undefinedtype>)$continuation).result; Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(((<undefinedtype>)$continuation).label) { case 0: ResultKt.throwOnFailure($result); ((<undefinedtype>)$continuation).label = 1; if (DelayKt.delay(1000L, (Continuation)$continuation) == var3) { return var3; } break; case 1: ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } return "100000"; } //无效的挂起函数 private static final Object noSuspendFun(Continuation $completion) { return "100000"; }
通过代码可以很清楚的看到suspendFun
和noSuspendFun
两个函数的区别,返回值可能是IntrinsicsKt.getCOROUTINE_SUSPENDED()也有可能是var10000 也可能是Unit.INSTANCE,也有可能是一个null,因此为了满足所有可能性使用Any?
是最合适的
为什么说Any?
是最合适的?
Kotlin中的Any类似于Java中的Object,Any是不可为空的,Any?是可以为空的,Any?包含Any的同时还包含了可空的类型,也就是说后者的包容性比前者更广,所以说前者就是后者的子类,同样的String和String?、Unit和Unit?也是一样的关系,用图表示就是这样
4.挂起函数的反编译
这里直接将上面suspendFun
函数反编译后的代码拿来分析
private static final Object suspendFun(Continuation var0) { Object $continuation; label20: { //undefinedtype就是Continuation //不是第一次进入走这里,保证只生成了一个实例 if (var0 instanceof <undefinedtype>) { $continuation = (<undefinedtype>)var0; if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) { ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE; break label20; } } //第一次进入走这里, $continuation = new ContinuationImpl(var0) { //协程返回结果 Object result; //表示协程状态机当前的状态 int label; //invokeSuspend 是协程的关键 //它最终会调用 suspendFun(this) 开启协程状态机 //状态机相关代码就是后面的 switch 语句 //协程的本质,可以说就是 CPS + 状态机 @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return SuspendDemoKt.suspendFun(this); } }; } //取出执行的结果 Object $result = ((<undefinedtype>)$continuation).result; //返回是否被挂起的状态 Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(((<undefinedtype>)$continuation).label) { case 0: //异常判断 ResultKt.throwOnFailure($result); //这里将label的状态改成1,进入下一行delay(1000L)代码 ((<undefinedtype>)$continuation).label = 1; if (DelayKt.delay(1000L, (Continuation)$continuation) == var3) { return var3; } break; case 1: ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } return "100000"; }
这里先对几个变量、函数进行说明:
- undefinedtype根据上下问的代码可以轻松的推断出来就是Continuation;
- label 是用来代表协程状态机当中状态的;
- result 是用来存储当前挂起函数执行结果的;
- invokeSuspend 这个函数,是整个状态机的入口,它会将执行流程转交给 suspendFun() 进行再次调用。
反编译的代码读起来比较费劲,因为原本提供的挂起函数代码的例子比较简单所以慢慢分析的话还是比较好理解的。
这里首先分析第一段代码的作用,根据上面的注释我将undefinedtype
修改为Continueation
label20: { //undefinedtype就是Continuation //不是第一次进入走这里,保证只生成了一个实例 if (var0 instanceof Continuation) { $continuation = var0; if ((($continuation).label & Integer.MIN_VALUE) != 0) { ($continuation).label -= Integer.MIN_VALUE; break label20; } } //第一次进入走这里, $continuation = new ContinuationImpl(var0) { //协程返回结果 Object result; //表示协程状态机当前的状态 int label; //invokeSuspend 是协程的关键 //它最终会调用 suspendFun(this) 开启协程状态机 //状态机相关代码就是后面的 switch 语句 //协程的本质,可以说就是 CPS + 状态机 @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return SuspendDemoKt.suspendFun(this); } }; }
ContinuationImpl是整个协程挂起函数的核心,挂起函数的状态机扩展自这个类。
第4行代码首先判断了var0是不是Continuation的实例,如果是那就赋值给continuation,首次进入时var0的值是空,因为它还没有被创建,会进入第13行代码执行,这相当于用一个新的 Continuation 包装了旧的 Continuation,整个过程中只会创建一个Continuation实例,节省了内存的开销。
invokeSuspend内部取出结果,给label设定初始值,然后开启协程的状态机,协程状态机的处理过程在switch中
//取出执行的结果 Object $result = $continuation.result; //返回是否被挂起的状态 Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch($continuation.label) { case 0: //异常判断 ResultKt.throwOnFailure($result); //这里将label的状态改成1,进入下一行delay(1000L)代码 $continuation.label = 1; if (DelayKt.delay(1000L, (Continuation)$continuation) == var3) { return var3; } break; case 1: ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } return "100000";
创建了Continuation的实例并且给result和label分别赋值,然后就是取出值了,switch是以label为依据进行处理的:
- case 0:在这里面首先进行异常判断,如果结果是失败,则抛出异常。然后这里将状态label改为1便于进入下一步处理,因为代码中第一行就是delay(1000L)所以在label = 0的时候就要去处理延迟函数的逻辑了:
DelayKt.delay是一个挂起函数,传入的参数分别是延迟时间和continuation的实例
DelayKt.delay函数在内部处理完毕后返回了IntrinsicsKt.COROUTINE_SUSPENDED
,这个值就是是否被挂起的标志,与var3进行判断,条件满足返回var3,case 0执行完毕进入case 1;
- case 1:进入case 1的第一步人就是判断是否有异常,然后因为原始代码中delay函数执行完毕后就立即返回了一个“100000”,所以case 1的代码也就到此为止。
以上就是对反编译代码的一个分析,因为原始代码比较简单因此反编译后的代码分析起来也相对简单,那么这里简单总结一下:
- switch实现了协程状态机,里面除了对不同情况下的状态的处理外还对状态进行了赋值的操作;
- continuation.label是状态流转的关键,continuation.label每改变一次就代表了挂起函数被调用了一次;
- 每次挂起函数执行完毕后都会检查是否发生异常;
- 如果一个函数被挂起了,它的返回值会是 CoroutineSingletons.COROUTINE_SUSPENDED;
上面的代码很简单,现在用一个较为复杂的代码再进行分析,验证一下上面总结的几点内容:
原始代码
suspend fun main() { val provincesCode = getProvincesCode() val cityCode = getCityCode(provincesCode) val areaCode = getAreaCode(cityCode) } /** * 获取省份Code * */ private suspend fun getProvincesCode(): String { withContext(Dispatchers.IO) { delay(1000L) } return "省:100000" } /** * 获取城市Code * * @param provincesCode */ private suspend fun getCityCode(provincesCode: String): String { withContext(Dispatchers.IO) { delay(1000L) } return "$provincesCode 市:100010" } /** * 获取区域code * * @param cityCode */ private suspend fun getAreaCode(cityCode: String): String { withContext(Dispatchers.IO) { delay(1000L) } return "$cityCode 区:100011" }
上面的代码反编译后的代码读起来更费劲,这里不对getProvincesCode()
、getCityCode(provincesCode)
、getAreaCode(cityCode)
三个函数进行分析因为跟上面的那段代码极为相似,这里主要分析main
函数中调用的逻辑:
public static final Object main(@NotNull Continuation var0) { Object $continuation; label37: { if (var0 instanceof <undefinedtype>) { $continuation = (<undefinedtype>)var0; if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) { ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE; break label37; } } $continuation = new ContinuationImpl(var0) { // $FF: synthetic field Object result; int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return RequestCodeKt.main((Continuation)this); } }; } Object var10000; label31: { Object var6; label30: { Object $result = ((<undefinedtype>)$continuation).result; var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(((<undefinedtype>)$continuation).label) { case 0: ResultKt.throwOnFailure($result); ((<undefinedtype>)$continuation).label = 1; var10000 = getProvincesCode((Continuation)$continuation); if (var10000 == var6) { return var6; } break; case 1: ResultKt.throwOnFailure($result); var10000 = $result; break; case 2: ResultKt.throwOnFailure($result); var10000 = $result; break label30; case 3: ResultKt.throwOnFailure($result); var10000 = $result; break label31; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } String provincesCode = (String)var10000; ((<undefinedtype>)$continuation).label = 2; var10000 = getCityCode(provincesCode, (Continuation)$continuation); if (var10000 == var6) { return var6; } } String cityCode = (String)var10000; ((<undefinedtype>)$continuation).label = 3; var10000 = getAreaCode(cityCode, (Continuation)$continuation); if (var10000 == var6) { return var6; } } String var3 = (String)var10000; return Unit.INSTANCE; }
这里的代码跟上面那个极为相似,保证只创建一个Continuation实例,然后通过label、var6、var10000做出不同的处理
- var6:挂起标志,返回
IntrinsicsKt.getCOROUTINE_SUSPENDED();
- var10000:
getProvincesCode()
、getCityCode(provincesCode)
、getAreaCode(cityCode)
都是挂起函数,因此返回结果中有执行结果和挂起标志; - label:label=1、2、3的情况主要都是在调用一个挂起函数的手被赋值,这也印证了上面总结的第二天条内容;
- switch:这个switch的流转仍旧是依靠label执行的,并且每次都会先进行异常判断。
第二段的代码分析结果就是对上面结论的验证,所以说无论复杂与否它的执行流程就是那几个,多进行分析就了解了,这个过程中一定要自己写,反编译,然后自己总结才能理解,单纯的看其实还是很费劲的。
这里还有一个点要关注一下,就是三个挂起函数中为什么都传入了continuation
,这是因为挂起函数被反编译后原本的suspend变成了Continueation参数,因此main函数也就必须是挂起函数,所以为什么说普通函数不能调用挂起函数,就是因为没有Continuation这个参数。
5.非挂起函数的分析
前面在分析CPS转换后返回值为什么是Any?时提出过非挂起函数,那么非挂起函数的处理流程是怎样的呢,将上面的代码进行修改,保留suspend,删除挂起函数的相关代码:
suspend fun main() { val provincesCode = getProvincesCode() val cityCode = getCityCode(provincesCode) val areaCode = getAreaCode(cityCode) } /** * 获取省份Code * */ private suspend fun getProvincesCode(): String { withContext(Dispatchers.IO) { delay(1000L) } return "省:100000" } /** * 获取城市Code * * @param provincesCode */ private suspend fun getCityCode(provincesCode: String): String { //变化在这里,删除了withContext和delay函数 return "$provincesCode 市:100010" } /** * 获取区域code * * @param cityCode */ private suspend fun getAreaCode(cityCode: String): String { withContext(Dispatchers.IO) { delay(1000L) } return "$cityCode 区:100011" }
反编译后的代码唯一变化点在getCityCode
private static final Object getCityCode(String provincesCode, Continuation $completion) { return provincesCode + " 市:100010"; }
反编译后的代码变得极为简单,在getCityCode函数中没有了状态机的流转而是直接返回了结果。
以上内容就是挂起函数的执行流程,那么它的原理用一句话总结:Kotlin的挂起函数本质上就是一个状态机;
以上就是Kotlin 挂起函数CPS转换原理解析的详细内容,更多关于Kotlin 挂起函数CPS转换的资料请关注脚本之家其它相关文章!