Kotlin协程实现原理:CoroutineScope,看完不懂你砍我!墙裂建议收藏。

2023-11-16

今天我们来聊聊Kotlin的协程Coroutine。文末有为大家准备的彩蛋。

如果你还没有接触过协程,推荐你先阅读这篇入门级文章What? 你还不知道Kotlin Coroutine?

如果你已经接触过协程,相信你都有过以下几个疑问:

  1. 协程到底是个什么东西?
  2. 协程的suspend有什么作用,工作原理是怎样的?
  3. 协程中的一些关键名称(例如:JobCoroutineDispatcherCoroutineContextCoroutineScope)它们之间到底是怎么样的关系?
  4. 协程的所谓非阻塞式挂起与恢复又是什么?
  5. 协程的内部实现原理是怎么样的?

接下来的一些文章试着来分析一下这些疑问,也欢迎大家一起加入来讨论。

协程是什么

这个疑问很简单,只要你不是野路子接触协程的,都应该能够知道。因为官方文档中已经明确给出了定义。

下面来看下官方的原话(也是这篇文章最具有底气的一段话)。

协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。

敲黑板划重点:协程是一种并发的设计模式。

所以并不是一些人所说的什么线程的另一种表现。虽然协程的内部也使用到了线程。但它更大的作用是它的设计思想。将我们传统的Callback回调方式进行消除。将异步编程趋近于同步对齐。

解释了这么多,最后我们还是直接点,来看下它的优点

  1. 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
  2. 内存泄露更少:使用结构化并发机制在一个作用域内执行多个操作。
  3. 内置取消支持:取消功能会自动通过正在运行的协程层次结构传播。
  4. Jetpack集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。

suspend

suspend是协程的关键字,每一个被suspend修饰的方法都必须在另一个suspend函数或者Coroutine协程程序中进行调用。

第一次看到这个定义不知道你们是否有疑问,反正小憩我是很疑惑,为什么suspend修饰的方法需要有这个限制呢?不加为什么就不可以,它的作用到底是什么?

当然,如果你有关注我之前的文章,应该就会有所了解,因为在重温Retrofit源码,笑看协程实现这篇文章中我已经有简单的提及。

这里涉及到一种机制俗称CPS(Continuation-Passing-Style)。每一个suspend修饰的方法或者lambda表达式都会在代码调用的时候为其额外添加Continuation类型的参数。

@GET("/v2/news")
suspend fun newsGet(@QueryMap params: Map<String, String>): NewsResponse

上面这段代码经过CPS转换之后真正的面目是这样的

@GET("/v2/news")
fun newsGet(@QueryMap params: Map<String, String>, c: Continuation<NewsResponse>): Any?

经过转换之后,原有的返回类型NewsResponse被添加到新增的Continutation参数中,同时返回了Any?类型。这里可能会有所疑问?返回类型都变了,结果不就出错了吗?

其实不是,Any?Kotlin中比较特殊,它可以代表任意类型。

suspend函数被协程挂起时,它会返回一个特殊的标识COROUTINE_SUSPENDED,而它本质就是一个Any;当协程不挂起进行执行时,它将返回执行的结果或者引发的异常。这样为了让这两种情况的返回都支持,所以使用了Kotlin独有的Any?类型。

返回值搞明白了,现在来说说这个Continutation参数。

首先来看下Continutation的源码

public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

context是协程的上下文,它更多时候是CombinedContext类型,类似于协程的集合,这个后续会详情说明。

resumeWith是用来唤醒挂起的协程。前面已经说过协程在执行的过程中,为了防止阻塞使用了挂起的特性,一旦协程内部的逻辑执行完毕之后,就是通过该方法来唤起协程。让它在之前挂起的位置继续执行下去。

所以每一个被suspend修饰的函数都会获取上层的Continutation,并将其作为参数传递给自己。既然是从上层传递过来的,那么Continutation是由谁创建的呢?

其实也不难猜到,Continutation就是与协程创建的时候一起被创建的。

GlobalScope.launch { 

}

launch的时候就已经创建了Continutation对象,并且启动了协程。所以在它里面进行挂起的协程传递的参数都是这个对象。

简单的理解就是协程使用resumeWith替换传统的callback,每一个协程程序的创建都会伴随Continutation的存在,同时协程创建的时候都会自动回调一次ContinutationresumeWith方法,以便让协程开始执行。

CoroutineContext

协程的上下文,它包含用户定义的一些数据集合,这些数据与协程密切相关。它类似于map集合,可以通过key来获取不同类型的数据。同时CoroutineContext的灵活性很强,如果其需要改变只需使用当前的CoroutineContext来创建一个新的CoroutineContext即可。

来看下CoroutineContext的定义

public interface CoroutineContext {
    /**
     * Returns the element with the given [key] from this context or `null`.
     */
    public operator fun <E : Element> get(key: Key<E>): E?

    /**
     * Accumulates entries of this context starting with [initial] value and applying [operation]
     * from left to right to current accumulator value and each element of this context.
     */
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext = ...

    /**
     * Returns a context containing elements from this context, but without an element with
     * the specified [key].
     */
    public fun minusKey(key: Key<*>): CoroutineContext

    /**
     * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
     */
    public interface Key<E : Element>

    /**
     * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
     */
    public interface Element : CoroutineContext {..}
}

每一个CoroutineContext都有它唯一的一个Key其中的类型是Element,我们可以通过对应的Key来获取对应的具体对象。说的有点抽象我们直接通过例子来了解。

var context = Job() + Dispatchers.IO + CoroutineName("aa")
LogUtils.d("$context, ${context[CoroutineName]}")
context = context.minusKey(Job)
LogUtils.d("$context")
// 输出
[JobImpl{Active}@158b42c, CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]], CoroutineName(aa)
[CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]]

JobDispatchersCoroutineName都实现了Element接口。

如果需要结合不同的CoroutineContext可以直接通过+拼接,本质就是使用了plus方法。

    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

plus的实现逻辑是将两个拼接的CoroutineContext封装到CombinedContext中组成一个拼接链,同时每次都将ContinuationInterceptor添加到拼接链的最尾部.

那么CombinedContext又是什么呢?

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {

    override fun <E : Element> get(key: Key<E>): E? {
        var cur = this
        while (true) {
            cur.element[key]?.let { return it }
            val next = cur.left
            if (next is CombinedContext) {
                cur = next
            } else {
                return next[key]
            }
        }
    }
    ...
}

注意看它的两个参数,我们直接拿上面的例子来分析

Job() + Dispatchers.IO
(Job, Dispatchers.IO)

Job对应于leftDispatchers.IO对应element。如果再拼接一层CoroutineName(aa)就是这样的

((Job, Dispatchers.IO),CoroutineName)

功能类似与链表,但不同的是你能够拿到上一个与你相连的整体内容。与之对应的就是minusKey方法,从集合中移除对应KeyCoroutineContext实例。

有了这个基础,我们再看它的get方法就很清晰了。先从element中去取,没有再从之前的left中取。

那么这个Key到底是什么呢?我们来看下CoroutineName

public data class CoroutineName(
    /**
     * User-defined coroutine name.
     */
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    /**
     * Key for [CoroutineName] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineName>

    /**
     * Returns a string representation of the object.
     */
    override fun toString(): String = "CoroutineName($name)"
}

很简单它的Key就是CoroutineContext.Key<CoroutineName>,当然这样还不够,需要继续结合对于的operator get方法,所以我们再来看下Elementget方法

public override operator fun <E : Element> get(key: Key<E>): E? =
    @Suppress("UNCHECKED_CAST")
    if (this.key == key) this as E else null

这里使用到了Kotlinoperator操作符重载的特性。那么下面的代码就是等效的。

context.get(CoroutineName)
context[CoroutineName]

所以我们就可以直接通过类似于Map的方式来获取整个协程中CoroutineContext集合中对应KeyCoroutineContext实例。

本篇文章主要介绍了suspend的工作原理与CoroutineContext的内部结构。希望对学习协程的伙伴们能够有所帮助,敬请期待后续的协程分析。

项目

android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。

AwesomeGithub: 基于Github客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于Jetpack&DataBindingMVVM;项目中使用了ArouterRetrofitCoroutineGlideDaggerHilt等流行开源技术。

flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。

android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。

daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。

Kotlin从零到整体系化学习

为什么要学Kotlin?未来大趋势?还是真香呢?

我个人认为:在不牺牲性能或安全性的前提下,许多的Kotlin功能使代码比Java更加简洁易懂。
Kotlin编译为字节码,因此其性能与Java一样好。它具有与Java相同的编译时检查(还有更多内容,例如内置的可空性检查)。最重要的是,Kotlin的语言功能和标准库功能可实现简洁有效的代码。

简洁,因为这是提高程序员工作效率的关键因素。

最初是组装。每行代码仅给您提供整个程序的功能说明。这使得读取和写入都变得困难,因为你必须一次将如此多的代码保存在脑海中。

高级语言使我们可以在每一行代码中添加更多想法。例如,对列表进行排序在大多数现代语言中都是微不足道的。当每行代码获得更多功能时,编写较大的程序会更容易。

但是,我们不想在每行代码中尽可能多地包含想法。如果代码过于简洁,那么它也将变得难以理解。正则表达式就是这个问题的典型例子。简而言之,我仍然会定期与之抗争。

综上,秉持着对Kotlin的热爱,也希望更多的朋友更好的、无痛的上手Kotlin,特将收录整理的《Kotlin从零到整学习笔记》图片中压缩包是大量的Kotlin实战案例,免费分享给大家。大家也可以在我的主页,找到我在B站的Android高阶教学视频更新地址


需要的朋友可以在评论区留下您的邮箱地址,我定期会抽空看评论,然后发给大家。如回复不及时,大家也可以在我的主页介绍处加上我的微信,备注【KT】无偿分享

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Kotlin协程实现原理:CoroutineScope,看完不懂你砍我!墙裂建议收藏。 的相关文章

  • 在 Android 中处理多个回收器视图 [Kotlin]

    我遇到过这样的情况 一个布局上有 3 个 RecyclerView 他们以某种方式相互依赖 数据来自房间数据库 问题原型 问题陈述 假设您有类似 Floor1 Floor2 Floor3 等 的楼层 并且每个楼层内都有类似 Room1 Ro
  • Mac OS Flutter 构建应用程序包失败:密钥库格式无效

    自从一周以来 我一直在尝试从 flutter 应用程序创建 android 应用程序包 并且我一直遵循创建上传密钥库 https flutter dev docs deployment android给定 flutter 官方网站 在 Ma
  • 如何更改android上的hosts文件[关闭]

    Closed 这个问题是无关 help closed questions 目前不接受答案 我已经成功root了我的三星Galaxy Mini android 2 2 1 并认为我可以改变任何东西 就像root通常可以的那样 我想更改 And
  • 如何在 IntelliJ 中快速文档中换行文本?

    Ctrl Q 快速文档 后我看不到所有文本 我必须水平滚动才能看到所有内容 我无法使编辑器适合窗口 更新 问题只是符号定义 即 第一行 描述 块文本 正确换行 实际上 我不想调整此窗口的大小 因为我更喜欢将其保留在相同的位置以便快速参考 我
  • Android Market 公司注册

    抱歉 这个问题与编程无关 但我无处可问 我在 android 帮助中心和支持中询问 但没有任何回应 在 Android Market 开设公司帐户需要什么 我希望我的公司被视为卖家 他们将如何验证公司的身份 我需要向市场提供哪些文件 Tha
  • 动态改变ListView中TextView字体颜色

    我正在将 XML 文档绑定到自定义适配器 列表中的所有项目最初的字体颜色均为白色 XML 文档中的一个节点有一个我正在检查的属性 如果设置了该属性 我想将 ListView 中该项目的字体颜色更改为较深的颜色 我的代码似乎最初可以工作 但是
  • 文件路径在棒棒糖android中始终返回null

    这是我从内部存储 画廊 获取图像时的代码 在棒棒糖文件路径中返回始终为空 if requestCode PICK IMAGE if resultCode RESULT OK image successfully picked launchi
  • 在 Phonegap InAppBrowser 中显示加载指示器

    当使用以下代码加载页面时 我尝试在 Phonegap InAppBrowser 中显示加载指示器 var ref ref window open http www google com top location no ref addEven
  • 如何使用 Java 在 Android Wi-Fi 连接上设置 ProxySettings 和 ProxyProperties?

    如何使用 Java 以编程方式 在 Android Wi Fi 连接上设置 ProxySettings 和 ProxyProperties 由于 ipAssignment linkProperties ProxySettings 和 Pro
  • 通过 adb 将触摸事件发送到设备[重复]

    这个问题在这里已经有答案了 我正在尝试使用以下方式将触摸事件发送到设备adb shell命令 这样我就可以为 UI 测试做一些基本的自动化 我已经关注了之前一些关于此问题的讨论 我确认了获取事件并使用sendevent 为每次触摸发送 6
  • 为什么找不到ImageView类?

    当我转到图形布局时 我在创建第一个 Android 应用程序 pdf Android Application Development for For Dummies 中的静默切换模式 时遇到了麻烦 在 main xml 文件中插入了 Ima
  • ActivityManager.getRunningTasks 已弃用 android

    我正在 android 中处理推送通知 我使用下面的方法来显示通知 但问题是现在 ActivityManager getRunningTasks 1 正在被弃用 从一个 stackoverflow 问题中我读到 你可以使用getAppTas
  • 渲染脚本渲染在Android上比OpenGL渲染慢很多

    背景 我想根据Android相机应用程序的代码添加实时滤镜 但Android相机应用程序的架构是基于OpenGL ES 1 x 我需要使用着色器来自定义我们的过滤器实现 然而 将相机应用程序更新到OpenGL ES 2 0太困难了 然后我必
  • 导航抽屉图标不显示 android

    MainActivity java public class MainActivity extends FragmentActivity public DrawerLayout mDrawerLayout public ListView m
  • gradle 构建工具版本从 1.2.3 升级到 1.3.1 的问题

    我已将 gradle 构建工具从 1 2 3 升级到 1 3 1 并开始在 gradle 同步上看到以下错误 我使用 1 2 3 版本没有任何问题 我使用的是 gradleVersion 2 3 无论如何我可以避免这个错误吗 错误 您的项目
  • Android 回调监听器 - 将 SDK 中的 pojo 的值发送到应用程序的 Activity

    我有一个深埋在 SDK 中的 java 类 它执行一个操作并返回一个布尔值 它不知道应用程序的主要活动 但我需要主要活动来接收该布尔值 我见过很多关于回调 广播和监听器的问题 但他们似乎都了解该活动 我的 pojo 确实有一个 Activi
  • ExpandableListView 和复选框

    我正在 Android 中编写简单的过滤器并想使用ExpandableListAdapter与复选框 我创建列表或检查复选框没有问题 但我真的不知道如何记住选择 关闭组并再次打开后或当我尝试打开不同的组时 复选框会发生变化 我尝试在网上阅读
  • 在每个 Activity 上调用工具栏

    我的应用程序有一个工具栏 应该出现在每个视图上 目前 我在我的onCreate 我有每个活动的方法 Toolbar toolbar Toolbar findViewById R id toolbar setSupportActionBar
  • 进度对话框未显示在屏幕上

    我根据亲爱的 Mayank answer 编辑了我的代码 但它没有显示在方法开始之前在 displayMsg 方法中作为输入发送的任何消息 我应该说 MethodTest 是通过 nfc 和 onNewIntent Intent Inten
  • 如何跟踪用户在我的 Android 应用程序上花费了多少时间?

    我想跟踪用户在我的 Android 应用程序上花费了多少时间 当用户在应用程序上处于活动状态时 我可以获取以小时为单位的时间吗 它会自动跟踪 Note 应用程序不在 Google Play 上 此代码将帮助您获取应用程序使用时间 long

随机推荐

  • 计算机网络学习笔记四、http和https

    http和https 从本篇文章开始总结http协议相关的知识点 http协议相关的内容可以分为四个部分 HTTP报文 HTTP请求 HTTP发展历史 HTTPS 1 HTTP报文 HTTP全称Hyper Text Transfer Pro
  • 数据库连接出错?

    数据库连接为什么会出错呢 NO suitable driver found for jdbc 问题源有3 1 连接的url的格式有错 Connection conn DriverManager getConnection jdbc sqls
  • 30个 JS 实用技巧总结,助你提升工作效率

    英文 https javascript plainenglish io 35 javascript short hands coding methodologies 28ea2d7d0a5e 翻译 杨小二 我是Rakshit Shah 我在
  • 【Java】JDK 1.8新特性

    Lambda 表达式 在没有 Lambda 表达式的时候 在 Java 中只能使用匿名内部类代替 Lambda 表达式 以下面的代码为例 查看 Lambda 表达式的使用 匿名内部类方式排序 List
  • c语言输入输出函数printf与scanf的用法格式

    c语言输入输出函数printf与scanf的用法格式 格式化规则例如 5 4f等类似问题的说明 Turbo C2 0 标准库提供了两个控制台格式化输入 输出函数printf 和scanf 这两个函数可以在标准输入输出设备上以各种不同的格式读
  • SAP LSMW日志信息如何导出到Excel里?

    SAP LSMW日志信息如何导出到Excel里 在SAP系统中 数据迁移LSMW运行的日志 是可以下载到本地Excel文件里的 方式如下所示 双击某个会话 点击打印机图标 就可以导出到Excel文件里了 输入文件名 指定文件保存的目录 完
  • 送书

    今天是周三 又到了给大家送书的时刻啦 这次给大家带来的是 OpenCV图像处理入门与实践 文末查看送书规则 简介 OpenCV 是一个开源的计算机视觉库 可以实现计算机视觉算法 本书从 OpenCV 用 Python 实现的基础语法讲起 逐
  • C51定时器和计数器 timer and counter

    代码 include
  • Seven Different Linux/BSD Firewalls Reviewed

    Seven Different Linux BSD Firewalls Reviewed Firewall November 14th 2007 Did you know more than 500 million computers in
  • 鲸鱼优化算法论文【matlab代码与仿真】

    一 算法流程 通过回声定位并相互传递探寻猎物信息 研究表明 鲸鱼大脑的某些区域与人类相似 有一种叫做梭形细胞的共同细胞 这些细胞负责人类的判断 情感和社会行为 这种细胞的数量是成年人的两倍 事实证明 鲸鱼可以像人类一样思考 学习 判断 交流
  • 从Kubernetes 1.14 发布,看技术社区演进方向

    Kubernetes 1 14 正式发布已经过去了一段时间 相信你已经从不同渠道看过了各种版本的解读 不过 相比于代码 Release 马上就要迎来5周岁生日的Kubernetes 项目接下来如何演进 其实也是一个让人着迷的话题 而作为一个
  • 在linux中,&和&&,

    对应刚接触linux命令的小伙伴们来说 这些符号一定是很困扰的下面我们一起来看这些符号区别和用法 表示任务在后台执行 如要在后台运行 如 root localhost local java jar test jar gt log txt 运
  • 大型 SaaS 平台产品架构设计

    当我们去搜索 架构 可以得到很多的架构图片 比如组织架构 业务架构 数据架构 技术架构 安全架构 产品架构 部署架构等 什么是架构 通常大家说架构一般指软件架构 架构是指软件的基础结构 创造这些基础结构的准则 以及对这些结构的描述 在这个定
  • IAR常见报错

    右键进入函数出错 project gt clean
  • 利用python进行数据分析之数据聚合和分组运算--小白笔记

    GroupBy机制 split apply combine 拆分 应用 合并 import pandas as pd import numpy as np df pd DataFrame key1 a a b b a key2 one tw
  • 找不到匹配的host key算法

    vim etc ssh sshd config
  • 《WebRTC系列》实战 Web 端支持 h265 硬解

    1 背景 Web 端实时预览 H 265 需求一直存在 但由于之前 Chrome 本身不支持 H 265 硬解 软解性能消耗大 仅能支持一路播放 该需求被搁置 去年 9 月份 Chrome 发布 M106 版本 默认开启 H 265 硬解
  • raw格式详解

    raw格式是camera sensor直接输出的格式 每个像素点表示一个颜色分量B G或R 注意 这句话不准确 红外相机的sensor和彩色相机的sensor有些不同 有的红外相机的sensor输出的raw data就是亮度值 即灰度值 输
  • Android Flutter开发环境搭建

    1 搭建 Flutter 开发环境 本栏亦在快速上手Android Flutter Flutter框架就不介绍了 框架这个东西怎么说呢 对于大部分人来说只是了解即可 如需了解的话 可以度娘资料很多 本节我们主要看下如何在Windwos下搭建
  • Kotlin协程实现原理:CoroutineScope,看完不懂你砍我!墙裂建议收藏。

    今天我们来聊聊Kotlin的协程Coroutine 文末有为大家准备的彩蛋 如果你还没有接触过协程 推荐你先阅读这篇入门级文章What 你还不知道Kotlin Coroutine 如果你已经接触过协程 相信你都有过以下几个疑问 协程到底是个