文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2022-12-08 20:58

关注

正文

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

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修改为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为依据进行处理的:

DelayKt.delay是一个挂起函数,传入的参数分别是延迟时间和continuation的实例

DelayKt.delay函数在内部处理完毕后返回了IntrinsicsKt.COROUTINE_SUSPENDED,这个值就是是否被挂起的标志,与var3进行判断,条件满足返回var3,case 0执行完毕进入case 1;

以上就是对反编译代码的一个分析,因为原始代码比较简单因此反编译后的代码分析起来也相对简单,那么这里简单总结一下:

上面的代码很简单,现在用一个较为复杂的代码再进行分析,验证一下上面总结的几点内容:

原始代码

suspend fun main() {
    val provincesCode = getProvincesCode()
    val cityCode = getCityCode(provincesCode)
    val areaCode = getAreaCode(cityCode)
}

private suspend fun getProvincesCode(): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "省:100000"
}

private suspend fun getCityCode(provincesCode: String): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "$provincesCode 市:100010"
}

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做出不同的处理

第二段的代码分析结果就是对上面结论的验证,所以说无论复杂与否它的执行流程就是那几个,多进行分析就了解了,这个过程中一定要自己写,反编译,然后自己总结才能理解,单纯的看其实还是很费劲的。

这里还有一个点要关注一下,就是三个挂起函数中为什么都传入了continuation,这是因为挂起函数被反编译后原本的suspend变成了Continueation参数,因此main函数也就必须是挂起函数,所以为什么说普通函数不能调用挂起函数,就是因为没有Continuation这个参数。

5.非挂起函数的分析

前面在分析CPS转换后返回值为什么是Any?时提出过非挂起函数,那么非挂起函数的处理流程是怎样的呢,将上面的代码进行修改,保留suspend,删除挂起函数的相关代码:

suspend fun main() {
    val provincesCode = getProvincesCode()
    val cityCode = getCityCode(provincesCode)
    val areaCode = getAreaCode(cityCode)
}

private suspend fun getProvincesCode(): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "省:100000"
}

private suspend fun getCityCode(provincesCode: String): String {
    //变化在这里,删除了withContext和delay函数
    return "$provincesCode 市:100010"
}

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转换的资料请关注编程网其它相关文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯