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

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

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

无糖可乐爱好者   2022-12-08 我要评论

正文

普通函数加上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的代码也就到此为止。

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们