Cursor blinking

Kotlin 协程挂起函数原理

Android 基础|协程|字数 424|阅读时长≈ 2 分钟
Kotlin 协程挂起函数原理

挂起函数

Kotlin 编译器将使用协程的代码转换成状态机的形式。这一过程主要涉及到挂起函数的调用。

通过一个示例来探讨挂起函数:

Code
class ExampleUnitTest_CoroutineCore {     @Test    fun test() {        runBlocking {            val scope = CoroutineScope(Job())            val job = scope.launch {                val msg = getNewMessage()                println(msg)            }            job.join()        }    }     suspend fun getNewMessage(): String {        return withContext(Dispatchers.IO) {            delay(1000)            "12345"        }    }     /**     * suspend 是灰色,如果内部没有其他挂起函数,那么编译器就会提示我们移除 suspend 标记     */    suspend fun testSuspend() {}}

反编译字节码如下:

Code
public final class ExampleUnitTest_CoroutineCore {    @Test    public final void test() {        BuildersKt.runBlocking$default((CoroutineContext)null, (Function2)(new Function2((Continuation)null) {            int label;             @Nullable            public final Object invokeSuspend(@NotNull Object $result) {            Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();            switch (this.label) {                case 0:                ResultKt.throwOnFailure($result);                CoroutineScope scope = CoroutineScopeKt.CoroutineScope((CoroutineContext)JobKt.Job$default((Job)null, 1, (Object)null));                Job job = BuildersKt.launch$default(scope, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {                    // $FF: synthetic field                    private Object L$0;                    int label;                     @Nullable                    public final Object invokeSuspend(@NotNull Object $result) {                    Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();                    Object var10000;                    CoroutineScope $this$launch;                    switch (this.label) {                        case 0:                        ResultKt.throwOnFailure($result);                        $this$launch = (CoroutineScope)this.L$0;                        ExampleUnitTest_CoroutineCore var5 = ExampleUnitTest_CoroutineCore.this;                        this.L$0 = $this$launch;                        this.label = 1;                        var10000 = var5.getNewMessage(this);                        if (var10000 == var4) {                            return var4;                        }                        break;                        case 1:                        $this$launch = (CoroutineScope)this.L$0;                        ResultKt.throwOnFailure($result);                        var10000 = $result;                        break;                        default:                        throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");                    }                     String msg = (String)var10000;                    LogExtKt.println($this$launch, msg);                    return Unit.INSTANCE;                }                     @NotNull                    public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {                    Intrinsics.checkNotNullParameter(completion, "completion");                    Function2 var3 = new <anonymous constructor>(completion);                    var3.L$0 = value;                    return var3;                }                     public final Object invoke(Object var1, Object var2) {                    return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);                }                }), 3, (Object)null);                this.label = 1;                if (job.join(this) == var4) {                    return var4;                }                break;                case 1:                ResultKt.throwOnFailure($result);                break;                default:                throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");            }             return Unit.INSTANCE;        }             @NotNull            public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {            Intrinsics.checkNotNullParameter(completion, "completion");            Function2 var3 = new <anonymous constructor>(completion);            return var3;        }             public final Object invoke(Object var1, Object var2) {            return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);        }        }), 1, (Object)null);    }     @Nullable    public final Object getNewMessage(@NotNull Continuation $completion) {        return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {            int label; // 默认 label=0            // 调用挂起            @Nullable            public final Object invokeSuspend(@NotNull Object $result) {            Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();            switch (this.label) {                case 0:                ResultKt.throwOnFailure($result); // throwOnFailure:如果结果失败则抛出异常,可以理解为对挂起函数做检测(取消也会抛出 CancellationException 异常)                this.label = 1;                // var2 是 COROUTINE_SUSPENDED,返回 var2 说明函数是挂起状态(相当于告诉父 callback,当前我内部已经在忙了,你可以先执行自己的事了,等我执行完再通知你)                // this 是 CancellableContinuation,在延迟任务执行完后通过 CancellableContinuation 回调回来(再次触发 invokeSuspend),第二次 label 已被修改为了 1;                if (DelayKt.delay(1000L, this) == var2) {                    return var2;                }                break;                case 1:                ResultKt.throwOnFailure($result); // 与上面一样,做异常检测,检测到异常抛出异常退出函数执行                break;                default:                throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");            }             return "12345"; // label=1,没发生异常,最终走到这里        }         @NotNull        public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {            Intrinsics.checkNotNullParameter(completion, "completion");            Function2 var3 = new <anonymous constructor>(completion);            return var3;        }        // Function2 接口的 invoke 函数        // 通过 create 创建一个 Continuation 对象,用于执行协程体逻辑,然后去调用 invokeSuspend() 方法从而获得本次的执行结果        // 已经有了 (Continuation)var2 为什么还要再创建一个?        // 因为 var2 BuildersKt.launch 的 Function2 匿名内部类创建并传递过来的,便于 getNewMessage 执行结果向上返回。类似于 func A ->func B(callbackA)->func C(callbackB)                    public final Object invoke(Object var1, Object var2) {            return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);        }         }), $completion);    }     /**     * Object 类型返回值用于确定该挂起函数是否真的被挂起     * 返回 Unit.INSTANCE 不需要挂起,     */    @Nullable    public final Object testSuspend(@NotNull Continuation $completion) {        return Unit.INSTANCE;    }}

从反编译字节码可以清晰看出挂起函数挂起和恢复的流程。挂起函数实现的本质还是回调,只是这层回调是编译器帮我们做了。

挂起与恢复的实现

挂起和恢复的实现是通过 Continuation(续体)来完成的。每个挂起函数都接受一个 Continuation 参数,这个参数包含了协程在挂起点之后继续执行的所有信息。

  • Continuation:续体是协程挂起和恢复的关键。它持有协程的上下文信息,包括协程在挂起时的状态和恢复执行时应当执行的代码位置。
  • 回调的转换:挂起函数通常会将异步操作转换为同步操作的形式。这是通过将 Continuation 传递给异步操作,然后在异步操作完成时调用 Continuation 的 resumeWith 方法来实现的。
  • 状态机转换:编译器将含有挂起点的协程代码转换成一个状态机(其实就 0、1 两个状态)。每个挂起点都对应状态机中的一个状态。编译器为协程生成的代码负责在挂起点之间转移,并在适当的时候恢复执行。
Code
public interface Continuation<in T> {    /**     * 协程的上下文     */    public val context: CoroutineContext     /**     * 恢复相应协程的执行,传递成功或失败的结果作为最后一个挂起点的返回值。     */    public fun resumeWith(result: Result<T>)}

参考文档

万字长文,写给Android工程师的协程指南