抽丝剥茧聊Kotlin协程之深入理解协程上下文CoroutineContext

本文主要是介绍抽丝剥茧聊Kotlin协程之深入理解协程上下文CoroutineContext,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. 前言

如果你对CoroutineContext不了解,本文值得你细细品读,如果一遍看不懂,不妨多读几遍。写作该文的过程也是我对CoroutineContext理解加深的过程。CoroutineContext是协程的基础,值得投入学习

Android开发者对Context都不陌生。在Android系统中,Context可谓神通广大,它可以获取应用资源,可以获取系统资源,可以启动Activity。Context有几个大名鼎鼎的子类,Activity、Application、Service,它们都是应用中非常重要的组件。

协程中也有个类似的概念,CoroutineContext。它是协程中的上下文,通过它我们可以控制协程在哪个线程中执行,可以设置协程的名字,可以用它来捕获协程抛出的异常等。

我们知道,通过CoroutineScope.launch方法可以启动一个协程。该方法第一个参数的类型就是CoroutineContext。默认值是EmptyCoroutineContext单例对象。

在开始讲解CoroutineContext之前我们来看一段协程中经常会遇到的代码

刚开始学协程的时候,我们经常会和Dispatchers.Main、Job、CoroutineName、CoroutineExceptionHandler打交道,它们都是CoroutineContext的子类。我们也很容易单独理解它们,Dispatchers.Main指把协程分发到主线程执行,Job可以管理协程的生命周期,CoroutineName可以设置协程的名字,CoroutineExceptionHandler可以捕获协程的异常。但是+操作符对大部分的Java开发者甚至Kotlin开发者而言会感觉到新鲜又难懂,在协程中CoroutineContext+到底是什么意思?

其实+操作符就是把两个CoroutineContext合并成一个链表,后文会详细讲解

2. CoroutineContext类图一览

根据类图结构我们可以把它分成四个层级:

  1. CoroutineContext 协程中所有上下文相关类的父接口。
  2. CombinedContext、Element、EmptyCoroutineContext。它们是CoroutineContext的直接子类。
  3. AbstractCoroutineContextElement、Job。这两个是Element的直接子类。
  4. CoroutineName、CoroutineExceptionHandler、CoroutineDispatcher(包含Dispatchers.Main和Dispatchers.Default)。它们是AbstractCoroutineContextElement的直接子类。

图中红框处,CombinedContext定义了size()和contains()方法,这与集合操作很像,CombinedContext是CoroutineContext对象的集合,而Element和EmptyCoroutineContext却没有定义这些方法,真正实现了集合操作的协程上下文只有CombinedContext,后文会详细讲解

3. CoroutineContext接口

CoroutineContext源码如下:

首先我们看下官方注释,我将它的作用归纳为:

Persistent context for the coroutine. It is an indexed set of [Element] instances. An indexed set is a mix between a set and a map. Every element in this set has a unique [Key].

  1. CoroutineContext是协程的上下文。
  2. CoroutineContext是element的set集合,没有重复类型的element对象。
  3. 集合中的每个element都有唯一的Key,Key可以用来检索元素。

相信大多数的人看到这样的解释时,都会心生疑惑,既然是set类型为啥不直接用HashSet来保存Element。CoroutineContext的实现原理又是什么呢?原因是考虑到协程嵌套,用链表实现更好。

接着我们来看下该接口定义的几个方法

4. Key接口

Key是一个接口定义在CoroutineContext中的一个接口,作为接口它没有声明任何的方法,那么其实它没有任何真正有用的意义,它只是用来检索。我们先来看下,协程库中是如何使用Key接口的。

通过观察协程官方库中的例子,我们发现Element的子类都必须重写Key这个属性,而且Key的泛型类型必须和类名相同。以CoroutineName为例,Key是一个伴生对象,同时Key的泛型类型也是CoroutineName。

为了方便理解,我仿照写了MyElement类,如下:

通过对比kt类和反编译的java类我们看到 Key就是一个静态变量,而且它的实现类,其实啥也没干。它的作用与HashMap中的Key类似:

  1. 实现key-value功能,为插入和删除提供检索功能
  2. Key是static静态变量,全局唯一,为Element提供唯一性保障

Kotlin语法糖

coroutineContext.get(CoroutineName.Key)

coroutineContext.get(CoroutineName)

coroutineContext[CoroutineName]

coroutineContext[CoroutineName.Key]

写法是等价的

4. CoroutineContext.get方法

源码(整理在一起,下同)

使用方式

讲解

通过Key检索Element。返回值只能是Element或者null,链表节点中的元素值。

  1. Element get方法:只要Key与当前Element的Key匹配上了,返回该Element否则返回null。
  2. CombinedContext get方法:遍历链表,查询与Key相等的Element,如果没找到返回null。

5. CoroutineContext.plus方法

源码

使用方式

讲解

将两个CoroutineContext组合成一个CoroutineContext,如果是两个类型相同的Element会返回一个新的Element。如果是两个不同类型的Element会返回一个CombinedContext。如果是多个不同类型的Element会返回一条CombinedContext链表。

我将上述算法总结成了5种场景,不过在介绍这5种场景前,我们先讲解CombinedContext的数据结构。

6. CombinedContext分析

因为CombinedContext是CoroutineContext的子类,left也是CoroutineContext类型的,所以它的数据结构是链表。我们经常用next来表示链表的下一个节点。那么为什么这里取名叫left呢?我甚至怀疑写这段代码的是个左撇子。真正的原因是,协程可以启动子协程,子协程又可以启动孙协程。父协程在左边,子协程在右边

嵌套启动协程

越是外层的协程的Context越在左边,大概示意图如下 (真实并非如此,比这更复杂)

链表的两个知识点在此都有体现。CoroutineContext.plus方法中使用的是头插法。CombinedContext的toString方法采用的是链表倒序打印法。

7. 五种plus场景

根据plus源码,我总结出会覆盖到五种场景。

  1. plus EmptyCoroutineContext
  2. plus 相同类型的Element
  3. plus方法的调用方没有Dispatcher相关的Element
  4. plus方法的调用方只有Dispatcher相关的Element
  5. plus方法的调用方是包含Dispatcher相关Element的链表

结果如下:

  1. Dispatchers.Main + EmptyCoroutineContext 结果:Dispatchers.Main
  2. CoroutineName("c1") + CoroutineName("c2")结果: CoroutineName("c2")。相同类型的直接替换掉。
  3. CoroutineName("c1") + Job()结果:CoroutineName("c1") <- Job。头插法被plus的(Job)放在链表头部
  4. Dispatchers.Main + Job()结果:Job <- Dispatchers.Main。虽然是头插法,但是ContinuationInterceptor必须在链表头部。
  5. Dispatchers.Main + Job() + CoroutineName("c5")结果:Job <- CoroutineName("c5") <- Dispatchers.Main。Dispatchers.Main在链表头部,其它的采用头插法。

如果不考虑Dispatchers.Main的情况。我们可以把+<-代替。CoroutineName("c1") + Job()等价于CoroutineName("c1") <- Job

8. CoroutineContext的minusKey方法

源码

讲解

  1. Element minusKey方法:如果Key与当前element的Key相等,返回EmptyCoroutineContext,否则相当于没减成功,返回当前element
  2. CombinedContext minusKey方法:删除链表中符合条件的节点,分三种情况。

三种情况以下面链表为例

Job <- CoroutineName(“c5”) <-Dispatchers.Main

  1. 没找到节点:minusKey(MyElement)。在Job节点处走newLeft === left分支,依此类推,在CoroutineName处走同样的分支,在Dispatchers.Main处走同样的分支。

  2. 节点在尾部:minusKey(Job)。在CoroutineName(“c5”)节点走newLeft === EmptyCoroutineContext分支,依此往头部递归

  3. 节点不在尾部:minusKey(CoroutineName)。在Dispatchers.Main节点处走else分支

9. 总结

学习CoroutineContext首先要搞清楚各类之间的继承关系,其次,CombinedContext各具体Element的集合,它的数据结构是链表,如果读者对链表增删改查操作熟悉的话,那么很容易就能搞懂CoroutineContext原理,否则想要搞懂CoroutineContext那简直如盲人摸象。

记得关注“字节小站”公众号哟~

这篇关于抽丝剥茧聊Kotlin协程之深入理解协程上下文CoroutineContext的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/737916

相关文章

Kotlin Map映射转换问题小结

《KotlinMap映射转换问题小结》文章介绍了Kotlin集合转换的多种方法,包括map(一对一转换)、mapIndexed(带索引)、mapNotNull(过滤null)、mapKeys/map... 目录Kotlin 集合转换:map、mapIndexed、mapNotNull、mapKeys、map

深入理解Go语言中二维切片的使用

《深入理解Go语言中二维切片的使用》本文深入讲解了Go语言中二维切片的概念与应用,用于表示矩阵、表格等二维数据结构,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧... 目录引言二维切片的基本概念定义创建二维切片二维切片的操作访问元素修改元素遍历二维切片二维切片的动态调整追加行动态

Android kotlin中 Channel 和 Flow 的区别和选择使用场景分析

《Androidkotlin中Channel和Flow的区别和选择使用场景分析》Kotlin协程中,Flow是冷数据流,按需触发,适合响应式数据处理;Channel是热数据流,持续发送,支持... 目录一、基本概念界定FlowChannel二、核心特性对比数据生产触发条件生产与消费的关系背压处理机制生命周期

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

一文深入详解Python的secrets模块

《一文深入详解Python的secrets模块》在构建涉及用户身份认证、权限管理、加密通信等系统时,开发者最不能忽视的一个问题就是“安全性”,Python在3.6版本中引入了专门面向安全用途的secr... 目录引言一、背景与动机:为什么需要 secrets 模块?二、secrets 模块的核心功能1. 基

Go学习记录之runtime包深入解析

《Go学习记录之runtime包深入解析》Go语言runtime包管理运行时环境,涵盖goroutine调度、内存分配、垃圾回收、类型信息等核心功能,:本文主要介绍Go学习记录之runtime包的... 目录前言:一、runtime包内容学习1、作用:① Goroutine和并发控制:② 垃圾回收:③ 栈和

深入解析 Java Future 类及代码示例

《深入解析JavaFuture类及代码示例》JavaFuture是java.util.concurrent包中用于表示异步计算结果的核心接口,下面给大家介绍JavaFuture类及实例代码,感兴... 目录一、Future 类概述二、核心工作机制代码示例执行流程2. 状态机模型3. 核心方法解析行为总结:三

Android学习总结之Java和kotlin区别超详细分析

《Android学习总结之Java和kotlin区别超详细分析》Java和Kotlin都是用于Android开发的编程语言,它们各自具有独特的特点和优势,:本文主要介绍Android学习总结之Ja... 目录一、空安全机制真题 1:Kotlin 如何解决 Java 的 NullPointerExceptio

Kotlin Compose Button 实现长按监听并实现动画效果(完整代码)

《KotlinComposeButton实现长按监听并实现动画效果(完整代码)》想要实现长按按钮开始录音,松开发送的功能,因此为了实现这些功能就需要自己写一个Button来解决问题,下面小编给大... 目录Button 实现原理1. Surface 的作用(关键)2. InteractionSource3.

Python多进程、多线程、协程典型示例解析(最新推荐)

《Python多进程、多线程、协程典型示例解析(最新推荐)》:本文主要介绍Python多进程、多线程、协程典型示例解析(最新推荐),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定... 目录一、multiprocessing(多进程)1. 模块简介2. 案例详解:并行计算平方和3. 实现逻