朱涛 · Kotlin 编程第一课
朱涛
Google 认证的 Kotlin、Android 开发者专家,博客“Kotlin Jetpack 实战”作者
6717 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 50 讲
朱涛 · Kotlin 编程第一课
15
15
1.0x
00:00/00:00
登录|注册

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
立即购买
登录 后留言

全部留言(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-12
    6
  • 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-10
    4
  • Shanks-王冲
    思考题:后知后觉 1. 最终还是运行了一遍,返回的是CoroutineSingletons.COROUTINE_SUSPENDED,即惊讶,又啊哈 2. 协程非阻塞,体现在suspend func立即返回个CoroutineSingletons.COROUTINE_SUSPENDED,然后继续忙其他的;于是伪suspend func,则直接返回结果 *关于如何在Java中调用:之前有课程提到,在Java中的getContext()中可考虑传入EmptyCoroutineContext.INSTANCE

    作者回复: 总结很到位~

    2022-03-30
    4
  • Allen
    关于思考题的思考: 在 Java 中是可以调用挂起函数的,当挂起函数通过 Kotlin 编译器进行 CPS 转换后,对应的挂起函数就被转换成了带 Callback 参数(Continuation 接口)的普通函数,只要传入 Continuation 接口的实现就可以了。

    作者回复: 说的很好,有代码就更好了

    2022-03-21
    3
  • 我不是很明白是怎么恢复协程,也就是调用resumewith,java反编译过去也没看到调用

    作者回复: 这里确实隐藏的比较深,需要写一个完整的例子,同时结合反编译代码+协程源码来分析。

    2022-05-04
    2
    1
  • 神秘嘉Bin
    HelloSuspendTestKt.hello(new Continuation<Integer>() { @NotNull @Override public CoroutineContext getContext() { return EmptyCoroutineContext.INSTANCE; } @Override public void resumeWith(@NotNull Object o) { // TODO } });

    作者回复: 不错~

    2022-03-24
    1
  • H.ZWei
    有点类似rxjava操作符的嵌套

    作者回复: 是的

    2022-04-28
  • 神佑小鹿
    1、invokeSuspend 不是协程的入口,是 resumeWith 的入口, 恢复协程是调用的 continuation.resumeWith,这个会调用 invokeSuspend

    作者回复: 可以这么理解。

    2022-04-11
  • 神佑小鹿
    这样看,每个挂起函数,都会创建一个继承了 ContinuationImpl 的匿名内部类对象,把传进来的 Continuation 包起来~~~

    作者回复: 粗略上,可以这么理解。

    2022-04-11
    2
  • 神佑小鹿
    withContext 为啥没有创建新的协程呢???我看源码都有对应的 job 和 coroutome.start

    作者回复: 协程数量是否加1,其实取决于newCoroutineContext()这个方法是否调用。

    2022-04-11
收起评论
显示
设置
留言
17
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部