Kotlin 挂起函数CPS转换原理解析

目录
  • 正文
    • 1.什么是CPS转换
    • 2.CPS的过程是怎么让参数改变的
    • 3.CPS的过程是怎么让返回值改变的
    • 4.挂起函数的反编译
    • 5.非挂起函数的分析

正文

普通函数加上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";
}

通过代码可以很清楚的看到suspendFunnoSuspendFun两个函数的区别,返回值可能是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 &lt;undefinedtype&gt;) {
            $continuation = (&lt;undefinedtype&gt;)var0;
            if ((((&lt;undefinedtype&gt;)$continuation).label &amp; Integer.MIN_VALUE) != 0) {
                ((&lt;undefinedtype&gt;)$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 = ((&lt;undefinedtype&gt;)$continuation).result;
            var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch(((&lt;undefinedtype&gt;)$continuation).label) {
                case 0:
                    ResultKt.throwOnFailure($result);
                    ((&lt;undefinedtype&gt;)$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;
            ((&lt;undefinedtype&gt;)$continuation).label = 2;
            var10000 = getCityCode(provincesCode, (Continuation)$continuation);
            if (var10000 == var6) {
                return var6;
            }
        }
        String cityCode = (String)var10000;
        ((&lt;undefinedtype&gt;)$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转换的资料请关注我们其它相关文章!

(0)

相关推荐

  • Kotlin Suspend挂起函数的使用详解

    目录 总结 本质 何时使用 消除回调 一些例子 总结 挂起(suspend)函数是所有协程的核心. 挂起函数可以执行长时间运行的操作并等待它完成而不会阻塞主线程. 挂起函数的语法与常规函数的语法类似,不同之处在于添加了suspend关键字. 它可以接受一个参数并有一个返回类型. 但是,挂起函数只能由另一个挂起函数或在协程内调用. suspend fun backgroundTask(param: Int): Int { // long running operation } 在背后,编译器将挂起

  • Kotlin挂起函数原理示例剖析

    目录 一.序言 二.CPS原理 CPS参数变化 CPS返回值变化 三.挂起函数的反编译 四.伪挂起函数 五.多个挂起函数前后关联 六.在Java中调用suspend函数 七.总结 一.序言 Kotlin挂起函数平时在学习和工作中用的比较多,掌握其原理还是很有必要的.本文将一步一步带着大家分析其原理实现. ps: 文中所用的Kotlin版本是1.7.0. 二.CPS原理 在某个Kotlin函数的前面加个suspend函数,它就成了挂起函数(虽然内部不一定会挂起,内部不挂起的称为伪挂起函数). 先随

  • Kotlin挂起函数的详细介绍

    Kotlin 协程的优势: 解决回调地狱的问题. 以同步的方式完成异步任务. 示例: fun main() { runBlocking { val a = getA() println(a) val b = getB(a) println(b) val c = getC(b) println(c) } } suspend fun getA(): String { withContext(Dispatchers.IO) { delay(2000L) } return "A content"

  • Kotlin 挂起函数CPS转换原理解析

    目录 正文 1.什么是CPS转换 2.CPS的过程是怎么让参数改变的 3.CPS的过程是怎么让返回值改变的 4.挂起函数的反编译 5.非挂起函数的分析 正文 普通函数加上suspend之后就成为了一个挂起函数,Kotlin编译器会将这个挂起函数转换成了带有参数Continuation<T>的一个普通函数,Continuation是一个接口,它跟Java中的Callback有着一样的功能,这个转换过程被称为CPS转换. 1.什么是CPS转换 挂起函数中的CPS转换就是把挂起函数转换成一个带有Ca

  • python中的函数递归和迭代原理解析

    这篇文章主要介绍了python中的函数递归和迭代原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.递归 1.递归的介绍 什么是递归? 程序调用自身的编程技巧称为递归( recursion).递归做为一种算法在程序设计语言中广泛应用. 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大

  • Java时区转换及Date类实现原理解析

    这篇文章主要介绍了Java时区转换及Date类实现原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.时区的说明 地球表面按经线从东到西,被划成一个个区域,规定相邻区域的时间相差1小时.在同一区域内的东端和西端的人看到太阳升起的时间最多相差不过1小时.当人们跨过一个区域,就将自己的时钟校正1小时(向西减1小时,向东加1小时),跨过几个区域就加或减几小时 ,所以同一时刻在不同时区表示的时间是不一样的. 二.时间的表示 我们平时表示时间时通

  • python next()和iter()函数原理解析

    这篇文章主要介绍了python next()和iter()函数原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 我们首先要知道什么是可迭代的对象(可以用for循环的对象)Iterable: 一类:list,tuple,dict,set,str 二类:generator,包含生成器和带yield的generatoe function 而生成器不但可以作用于for,还可以被next()函数不断调用并返回下一个值,可以被next()函数不断返回

  • python sorted函数原理解析及练习

    这篇文章主要介绍了python sorted函数原理解析及练习,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前两天学习了一下socket编程,在向某大神请教问题时被嫌弃了,有一种还没学会走就想跑的感觉.大神说我现在的水平应该去做一些像是操作文件.序列号等的小练习来加深理解.下面是他给我出的小练习: 1.datas = [['sherry',19,'female'],['flora',21,'female'],['june',15,'femal

  • Python文件操作及内置函数flush原理解析

    1.打开文件得到文件句柄并赋值给一个变量 2.通过句柄对文件进行操作 3.关闭文件 示例文件 '你好呀' 我是于超 嗯 再见 文件操作基本流程 f=open('chenli',encoding='utf-8') first_line=f.readline() print('第一行是:',first_line) print('我是分割线'.center(9,'-')) #9代表字符串总共9长度 data=f.read() #读取所有内容 print(data) #不指定打开编码,即python解释

  • javascript History对象原理解析

    这篇文章主要介绍了javascript History对象原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 length history.length属性保存着历史记录的URL数量.初始时,该值为1.由于IE10+浏览器在初始时返回2,存在兼容性问题,所以该值并不常用 跳转方法 go().back()和forward() 如果移动的位置超出了访问历史的边界,以上三个方法并不报错,而是静默失败 [注意]使用历史记录时,页面通常从浏览器缓存之

随机推荐