27 | 图解挂起函数:原来你就是个状态机?
朱涛
你好,我是朱涛。今天我们来研究 Kotlin 挂起函数的实现原理。
挂起函数,是整个 Kotlin 协程的核心,它的重要性不言而喻。几乎所有协程里的知识点,都离不开挂起函数。而且也正是因为挂起函数的原因,我们才可以使用协程简化异步任务。
今天这节课,我会从这个 CPS 转换开始说起,带你进一步挖掘它背后的细节。在这个过程中,我们还会接触到 Kotlin 库当中的协程基础元素:Continuation、CoroutineContext 与挂起函数的底层联系。最后,我会带你灵活运用下这些知识点,以此进一步完善我们的 KtHttp,让它可以直接支持挂起函数。
好,接下来,我们就正式开始吧!
CPS 转换背后的细节
在第 15 讲当中,我们已经初步介绍过挂起函数的用法了:挂起函数,只是比普通的函数多了 suspend 关键字。有了这个 suspend 关键字以后,Kotlin 编译器就会特殊对待这个函数,将其转换成一个带有 Callback 的函数,这里的 Callback 就是 Continuation 接口。
而这个过程,我们称之为 CPS 转换:
以上的 CPS 转换过程中,函数的类型发生了变化:suspend ()->String 变成了 (Continuation)-> Any?。这意味着,如果你在 Java 里访问一个 Kotlin 挂起函数 getUserInfo(),会看到 getUserInfo() 的类型是 (Continuation)-> Object,也就是:接收 Continuation 为参数,返回值是 Object。
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
Kotlin协程中的挂起函数是协程的核心,通过CPS转换将其转换成带有Callback的函数。挂起函数的参数和返回值发生变化,参数会带上Continuation作为最后一个参数,返回值类型变成了Any?。文章深入探讨了挂起函数的底层实现原理,帮助读者更清晰地理解挂起函数。通过分析挂起函数的反编译代码,深入探讨了协程状态机的核心逻辑,展示了Kotlin编译器是如何创建匿名内部类来处理协程的挂起和恢复。此外,文章还详细解释了协程状态机的运行流程,以及挂起函数执行过程中的状态转换和结果保存。通过这些内容,读者可以更好地理解挂起函数的内部工作原理。文章还介绍了Kotlin协程的基础元素,如Continuation接口和suspend inline val coroutineContext变量,帮助读者更全面地了解Kotlin协程的基础概念。整体而言,本文通过深入分析挂起函数的底层实现原理,帮助读者更好地理解了Kotlin协程中挂起函数的工作机制。文章还探讨了KtHttp对挂起函数的支持,展示了如何让KtHttp直接支持挂起函数,为读者提供了实际应用的示例。文章内容详实,逻辑清晰,对于想要深入了解Kotlin协程内部原理的读者来说,是一篇非常有价值的文章。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《朱涛 · Kotlin 编程第一课》,新⼈⾸单¥59
《朱涛 · Kotlin 编程第一课》,新⼈⾸单¥59
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(17)
- 最新
- 精选
- 神佑小鹿我刚看了字节码,总结如下: 可以看出: 1、testCoroutine 内部会创建一个匿名内部类 wrapper 实例,继承了 ContinuationImpl 类,将参数 Continuation var0 包在的内部;并且会将该 wrapper 实例作为参数传递给内部的挂起函数 getUserInfo... ...; ContinuationImpl 继承了 BaseContinuationImpl,间接实现了 Continuation 接口; 协程的恢复,就是通过 BaseContinuationImpl.resumeWith 实现,resumeWith 又会执行 invokeSuspend 方法,将目标结果返回; invokeSuspend 是协程恢复的入口,其内部又会执行挂起函数 testCoroutine,而此时的参数是 wrapper 实例; 2、匿名内部类 wrapper 实例内部的 label 变量表示:状态机的状态,而状态的流转逻辑是由挂起函数 testCoroutine 控制; 匿名内部类 wrapper 实例内部的 result 变量表示:目标结果; 3、匿名内部类 wrapper 实例只会创建一次!! 当第一次调用 testCoroutine 的时候,参数 Continuation var0 是外部的 Continuation 实例(外部协程的 Continuation/外部挂起函数的 ContinuationWrapper 实例) 当挂起后恢复,再次调用的时候,参数 Continuation var0 是第一次创建的 匿名内部类 wrapper 实例; 4、testCoroutine 状态机结构是通过 switch 语句 + label 代码段嵌套 testCoroutine 里的原本的代码,被拆分到状态机里各个状态中,分开执行; 每次挂起函数执行完,恢复后,都会检查是否发生异常; 如果一个挂起函数挂起,它的返回值会是 CoroutineSingletons.COROUTINE_SUSPENDED;
作者回复: 不错的总结~
2022-04-126 - Geek_Adr// JAVA 调用 挂起函数 public static void main(String[] args) throws InterruptedException { SuspendFromJavaExample.INSTANCE.getUserInfo(111, new Continuation<String>() { @NotNull @Override public CoroutineContext getContext() { return (CoroutineContext) Dispatchers.getDefault(); } @Override public void resumeWith(@NotNull Object o) { System.out.printf("suspend fun getUserInfo result => %s\n", o); } }); Thread.sleep(2000); // 有点挫,等结果 }
作者回复: 不错~ BTW:注释有意思,哈哈~
2022-04-104 - Shanks-王冲思考题:后知后觉 1. 最终还是运行了一遍,返回的是CoroutineSingletons.COROUTINE_SUSPENDED,即惊讶,又啊哈 2. 协程非阻塞,体现在suspend func立即返回个CoroutineSingletons.COROUTINE_SUSPENDED,然后继续忙其他的;于是伪suspend func,则直接返回结果 *关于如何在Java中调用:之前有课程提到,在Java中的getContext()中可考虑传入EmptyCoroutineContext.INSTANCE
作者回复: 总结很到位~
2022-03-304 - Allen关于思考题的思考: 在 Java 中是可以调用挂起函数的,当挂起函数通过 Kotlin 编译器进行 CPS 转换后,对应的挂起函数就被转换成了带 Callback 参数(Continuation 接口)的普通函数,只要传入 Continuation 接口的实现就可以了。
作者回复: 说的很好,有代码就更好了
2022-03-213 - 白我不是很明白是怎么恢复协程,也就是调用resumewith,java反编译过去也没看到调用
作者回复: 这里确实隐藏的比较深,需要写一个完整的例子,同时结合反编译代码+协程源码来分析。
2022-05-0421 - 神秘嘉BinHelloSuspendTestKt.hello(new Continuation<Integer>() { @NotNull @Override public CoroutineContext getContext() { return EmptyCoroutineContext.INSTANCE; } @Override public void resumeWith(@NotNull Object o) { // TODO } });
作者回复: 不错~
2022-03-241 - H.ZWei有点类似rxjava操作符的嵌套
作者回复: 是的
2022-04-28 - 神佑小鹿1、invokeSuspend 不是协程的入口,是 resumeWith 的入口, 恢复协程是调用的 continuation.resumeWith,这个会调用 invokeSuspend
作者回复: 可以这么理解。
2022-04-11 - 神佑小鹿这样看,每个挂起函数,都会创建一个继承了 ContinuationImpl 的匿名内部类对象,把传进来的 Continuation 包起来~~~
作者回复: 粗略上,可以这么理解。
2022-04-112 - 神佑小鹿withContext 为啥没有创建新的协程呢???我看源码都有对应的 job 和 coroutome.start
作者回复: 协程数量是否加1,其实取决于newCoroutineContext()这个方法是否调用。
2022-04-11
收起评论