Kotlin协程源码分析-协程的启动

1,672 阅读4分钟

1.简单启动协程Demo

本文例子代码地址:gitee.com/mcaotuman/k…

写个小demo,先记住怎么用,后面再分析源码。

image1.png

三个挂起函数:

image.png

输出结果:

image.png

2.构建suspend修饰的lambda函数

2.1 suspend lambda

下面这段代码是suspend修饰lambda表达式:

val mySuspendLambda: suspend () -> String = {
    //返回一个字符串hello world
    val one = commonSuspendFun()
    //返回一个字符串hello world2
    val two = commonSuspendFun2()
    //返回一个字符串hello world3
    val three = commonSuspendFun3()
    one+two+three
}

suspend lambda反编译成Java代码究竟长什么样子呢?

image1.jpg

我们用JEB工具反编译看看:

我们根据代码定位到包three下 image1.png

按右键解析字节码文件,得到反编译之后的Java代码: image.png

由上图我们知道suspend lambda经过编译器的黑魔法,会编译成SuspendLambda类(very important),继承关系如下图:

image.png

现在你看到BaseContinuationImpl类和Continuation接口,可能会一脸懵逼,究竟是干嘛的?没事,我们下面继续分析。

2.2 suspend函数

下面这段代码是suspend函数:

suspend fun commonSuspendFun(): String {
    return "[hello world] "
}

反编译成Java代码长成这个样子:

111.png 没了suspend关键字,多了一个Continuation类型的参数。
这个Continuation又出现了,上面小节提到SuspendLambda会实现Continuation接口
Continuation到底是个什么东西?

来,看看源码:
111.png

这个Continuation接口不就是类似我们常写的CallBack吗?Continuation的resumeWith方法不就是相当于CallBack的onSuccess吗?没错的。

这个从挂起函数转换成CallBack 函数的过程,被称为:CPS 转换(Continuation-Passing-Style Transformation)。

不知道你有没有发现,返回值由原来的String 变成 Object!

由于 suspend 修饰的函数,既可能返回 CoroutineSingletons.COROUTINE_SUSPENDED,也可能返回实际结果[hello world],甚至可能返回 null,为了适配所有的可能性,CPS 转换后的函数返回值类型就只能是 Object了。

3.创建协程:createCoroutine

我在如何查看Kotlin expect关键字在对应平台的实现(actual)? 这篇文章也曾提过createCoroutine的源码,他最后会调到createCoroutineUnintercepted方法里。

image.png

根据上面的分析,我们知道suspend lambda在编译器的黑魔法编译之后,会变成SuspendLambda类,实际上也是继承BaseContinuationImpl,那么他会直接走create方法。

实际上,是走到SuspendLambda类的构造函数

111.png

最后,走到BaseContinuationImpl,把SuspendLambda构建出来。 image.png

4.启动协程:resume

由步骤3得知,我们已经拿到了SuspendLambda实例,现在我们调用resume扩展函数启动协程。 image.png

实际上是调用ContinuationresumeWith方法

那么我们debug一下BaseContinuationImpl的resumeWith方法,就知道他是怎么启动协程的了。

image.png

CoroutineRunKt$testFunGetContinuation$mySuspendLambda$1对应suspend lambda对象的

mySuspendLambda.这个对象主要作用就是用来维护 状态机,这个也是kotlin 协程的关键。
我们跟踪一下CoroutineRunKt$testFunGetContinuation$mySuspendLambda$1的Java代码:

image1.png

jeb demo版不允许复制代码,蛋疼,只能截图。
于是我手动打上了代码,并注释关键步骤:

public final Object invokeSuspend(Object arg8) {
    String v3;
    CoroutineRunKt.testFunGetContinuation.mySuspendLambda.1 this;
    // v0:挂起函数返回标识SUSPEND_FLAG
    Object v0 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    // (一)label一开始默认是0
    switch (this.label) {
        case 0:
        ResultKt.throwOnFailure(arg8);
        // (二)将label设置为1
        this.label = 1;
        // (三)调用第一个挂起函数,传入自己作为参数
        // 如果返回SUSPEND_FLAG就返回函数。
        // 当挂起函数用传入的Continuation 调用resume时候又重新回到这个invokeSuspend方法
        // 当然我们这里由于不会返回SUSPEND_FLAG 所以继续向下运行
        Object v2 = CoroutineRunKt.commonSuspendFun((Continuation)this);
        if (v2 == v0) {
            return v0;
        }
        this = this;
        // 程序跳转到label_48
        goto label_48;
        case 1:
        ResultKt.throwOnFailure(arg8);
        this = this;
        label_48:
        this.L$0 = one;
        // (四)将label设置为2
        this.label = 2;
        //  (五) 调用挂起函数 与(三)相同
        Object v3_1 = CoroutineRunKt.commonSuspendFun2((Continuation)this);
        if (v3_1 == v0) {
            return v0;
        }
        v3 = one;
        arg8 = v3_1;
        // (六) 走到case 2
        label_60:
        String two = (String)arg8;
        this.L$0 = v3;
        this.L$1 = two;
        // (八)将label设置为3
        this.label = 3;
        // (九)调用最后一个挂起函数 同样与(三)一样检查结果
        Object v4 = CoroutineRunKt.commonSuspendFun3((Continuation)this);
        // (十)将挂起函数一、二、三返回的结果相加返回
        return v4 = v0 ? v0 : v3 +two +((String)v4);
        case 2:
        String v2_2 = (String) this.L$0;
        ResultKt.throwOnFailure(arg8);
        v3 = v2_2;
        this = this;
        // (七) 走到label_60
        goto label_60;
        case 3:
        Stirng v2_3 = (Stirng)this.L$1;
        Stirng v1 = (Stirng)this.L$0;
        ResultKt.throwOnFailure(arg8);
        return v1 + v2_3 +((Stirng)arg8);
        default:
        throw new IllgelStateException("call to 'resume' befor 'invoke' with coroutinue");
    }

}

看字节码反编译之后的代码,是不是有点头疼?我也觉得。我写了一个Java版的状态机实现Demo帮助大家理解。
代码地址:gitee.com/mcaotuman/k…

image.png

image.png

switch case表达式实现了协程的状态机模式,continuation.label是状态机的标志位,每改变一次,label加1,协程就切换1次。 一个函数如果被挂起了,它的返回值会是: CoroutineSingletons.COROUTINE_SUSPENDED,函数恢复后会继续当前label执行下去,直到所有label执行完。

4.结尾

终于写完了Kotlin协程启动源码分析了,其实把CPS转换和状态机搞懂之后,协程原理也挺容易接受的。本人水平有限,文章中如有错误或不妥之处在所难免。希望读者给予批评指正。谢谢Thanks♪(・ω・)ノ