前言
本篇文章是作为我的一个学习记录,写成文章也是为了更好的加深记忆和理解,也是为了分享知识。本文的定位是协程的稍微深入的全面知识,也会示例一些简单的使用,这里不对
suspend讲解,因为有更好的博文,下文中给出了链接,也不对协程的高级用法做阐述(热数据通道Channel、冷数据流Flow...),本文主要讲协程稍微深入的全面知识。
Kotlin Coroutine 简介
Kotlin 中的协程提供了一种全新处理并发的方式,您可以在 Android 平台上使用它来简化异步执行的代码。协程是从 Kotlin 1.3 版本开始引入,但这一概念在编程世界诞生的黎明之际就有了,最早使用协程的编程语言可以追溯到 1967 年的 Simula 语言。
在过去几年间,协程这个概念发展势头迅猛,现已经被诸多主流编程语言采用,比如 Javascript、C#、Python、Ruby 以及 Go 等。Kotlin 的协程是基于来自其他语言的既定概念。
在 Android 平台上,协程主要用来解决两个问题:
- 处理耗时任务 (Long running tasks),这种任务常常会阻塞住主线程;
 - 保证主线程安全 (Main-safety) ,即确保安全地从主线程调用任何 suspend 函数。
 
Kotlin Coroutine Version
Kotlin Version: 1.4.32
Coroutine Version: 1.4.3
Kotlin Coroutine 生态
kotlin的协程实现分为了两个层次:
- 基础设施层:
 
标准库的协程API,主要对协程提供了概念和语义上最基本的支持
- 业务框架层 kotlin.coroutines:
 
协程的上层框架支持,也是我们日常开发使用的库
接入Coroutine
1  | groovy复制代码dependencies {  | 
Coroutine 基本使用
suspend
关于
suspend挂起函数,这里不展开去讲,因为有更好的博文,那当然是扔物线凯哥的博文,最初我也是跟随凯哥的视频去学习的写成,大家可以去扔物线的网站去学习下协程的suspend或其他关于协程的知识,下面放上链接:
创建协程
创建协程的方式有很多种,这里不延伸协程的高级用法(热数据通道Channel、冷数据流Flow...),也许以后会在文章里补充或者新写文章来专门讲解,创建协程这里介绍常用的两种方式:
- CoroutineScope.launch()
 - CoroutineScope.async()
 这是常用的协程创建方式,launch 构建器适合执行 “一劳永逸” 的工作,意思就是说它可以启动新协程而不将结果返回给调用方;async 构建器可启动新协程并允许您使用一个名为
await的挂起函数返回result。 launch 和 async 之间的很大差异是它们对异常的处理方式不同。如果使用 async 作为最外层协程的开启方式,它期望最终是通过调用await来获取结果 (或者异常),所以默认情况下它不会抛出异常。这意味着如果使用 async 启动新的最外层协程,而不使用await,它会静默地将异常丢弃。
CoroutineScope.launch()
直接上代码:
1  | kotlin复制代码import androidx.appcompat.app.AppCompatActivity  | 
上面的代码中,给出了一些代码示例,其实协程的简单使用非常简单,你甚至完全不需要担心其他的东西,你只需要记得及时取消协程就ok,如果你使用lifecycleScope或者viewModelScope你连取消都不用自己管,界面或ViewModel被销毁时,会自动帮你把协程取消掉。使用协程只需要会创建、会切线程、懂四种调度模式,基本就ok了,基本开发已满足。
CoroutineScope.async()
async主要用于获取返回值和并发,直接上代码:
1  | kotlin复制代码fun asyncTest() {  | 
上面的代码主要展示async的返回值功能,需要与await()挂起函数结合使用
下面展示async的并发能力:
1  | kotlin复制代码fun asyncTest2() {  | 
上面的代码就是一个简单的并发示例,是不是感觉十分的简单,协程的优势立马凸显出来了。
这就是最基本的协程使用,关于作用域,更推荐的是在UI组件中使用LifecycleOwner.lifecycleScope,在ViewModel中使用ViewModel.viewModelScope
Coroutine的深入
其实简单的使用,就已经满足大部分日常开发需求,但是我们有必要全面了解一下
Coroutine,以便能够排查问题及自定义场景,下面我们从一个最基本的函数来切入,这个函数就是launch{}:
1  | kotlin复制代码public fun CoroutineScope.launch(  | 
上面是launch函数的定义,它以CoroutineScope的扩展函数的形成出现,函数参数分别是:协程上下文CoroutineContext、协程启动模式CoroutineStart、协程体,返回值是协程实例Job,其中CoroutineContext又包括了Job、CoroutineDispatcher、CoroutineName。下面我们就一一介绍这些内容:CoroutineContext、Job、CoroutineDispatcher、CoroutineStart、CoroutineScope。
CoroutineContext - 协程上下文
CoroutineContext即协程的上下文,是 Kotlin 协程的一个基本结构单元。巧妙的运用协程上下文是至关重要的,以此来实现正确的线程行为、生命周期、异常以及调试。它包含用户定义的一些数据集合,这些数据与协程密切相关。它是一个有索引的Element实例集合。这个有索引的集合类似于一个介于set和 map之间的数据结构。每个element在这个集合有一个唯一的 Key 。当多个element的 key 的引用相同,则代表属于集合里同一个element。它由如下几项构成:
- Job: 控制协程的生命周期;
 - CoroutineDispatcher: 向合适的线程分发任务;
 - CoroutineName: 协程的名称,调试的时候很有用;
 - CoroutineExceptionHandler: 处理未被捕捉的异常。
 
CoroutineContext有两个非常重要的元素 —Job和Dispatcher,Job是当前的Coroutine实例而Dispatcher决定了当前Coroutine执行的线程,还可以添加CoroutineName,用于调试,添加CoroutineExceptionHandler用于捕获异常,它们都实现了Element接口。看一个例子:
1  | kotlin复制代码fun main() {  | 
输出结果如下:
1  | scss复制代码[JobImpl{Active}@7eda2dbb, CoroutineName(myContext), Dispatchers.Default],CoroutineName(myContext)  | 
CoroutineContext接口的定义如下:
1  | kotlin复制代码public interface CoroutineContext {  | 
CoroutineContext 定义了四个核心的操作:
- 操作符get
 
可以通过 key 来获取这个 Element。由于这是一个 get 操作符,所以可以像访问 map 中的元素一样使用 context[key] 这种中括号的形式来访问。
- 操作符 plus
 
和 Set.plus 扩展函数类似,返回一个新的 context 对象,新的对象里面包含了两个里面的所有 Element,如果遇到重复的(Key 一样的),那么用+号右边的 Element 替代左边的。+ 运算符可以很容易的用于结合上下文,但是有一个很重要的事情需要小心 —— 要注意它们结合的次序,因为这个 + 运算符是不对称的。
- fun fold(initial: R, operation: (R, Element) -> R): R
 
和 Collection.fold 扩展函数类似,提供遍历当前 context 中所有 Element 的能力。
- fun minusKey(key: Key<*>): CoroutineContext
 
返回一个上下文,其中包含该上下文中的元素,但不包含具有指定key的元素。
某些情况需要一个上下文不持有任何元素,此时就可以使用 EmptyCoroutineContext 对象。可以预见,添加这个对象到另一个上下文不会对其有任何影响。
在任务层级中,每个协程都会有一个父级对象,要么是
CoroutineScope或者另外一个coroutine。然而,实际上协程的父级CoroutineContext和父级协程的CoroutineContext是不一样的,因为有如下的公式:
父级上下文 = 默认值 + 继承的 CoroutineContext + 参数
其中:
- 一些元素包含默认值: Dispatchers.Default 是默认的 CoroutineDispatcher,以及 “coroutine” 作为默认的 CoroutineName;
 - 继承的 CoroutineContext 是 CoroutineScope 或者其父协程的 CoroutineContext;
 - 传入协程 builder 的参数的优先级高于继承的上下文参数,因此会覆盖对应的参数值。
 
请注意: CoroutineContext 可以使用 “ + “ 运算符进行合并。由于 CoroutineContext 是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext。比如,(Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")。
Job & Deferred - 任务
Job用于处理协程。对于每一个所创建的协程 (通过launch或者 async),它会返回一个Job实例,该实例是协程的唯一标识,并且负责管理协程的生命周期
CoroutineScope.launch函数返回的是一个Job对象,代表一个异步的任务。Job具有生命周期并且可以取消。Job还可以有层级关系,一个Job可以包含多个子Job,当父Job被取消后,所有的子Job也会被自动取消;当子Job被取消或者出现异常后父Job也会被取消。除了通过
CoroutineScope.launch来创建Job对象之外,还可以通过Job()工厂方法来创建该对象。默认情况下,子Job的失败将会导致父Job被取消,这种默认的行为可以通过SupervisorJob来修改。具有多个子
Job的父Job会等待所有子Job完成(或者取消)后,自己才会执行完成
Job 的状态
一个任务可以包含一系列状态: 新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。虽然我们无法直接访问这些状态,但是我们可以访问
Job的属性:isActive、isCancelled和isCompleted。如果协程处于活跃状态,协程运行出错或者调用
job.cancel()都会将当前任务置为取消中 (Cancelling) 状态 (isActive = false, isCancelled = true)。当所有的子协程都完成后,协程会进入已取消 (Cancelled) 状态,此时isCompleted = true。
| State | [isActive] | [isCompleted] | [isCancelled] | 
|---|---|---|---|
| New (optional initial state) | false | 
false | 
false | 
| Active (default initial state) | true | 
false | 
false | 
| Completing (transient state) | true | 
false | 
false | 
| Cancelling (transient state) | false | 
false | 
true | 
| Cancelled (final state) | false | 
true | 
true | 
| Completed (final state) | false | 
true | 
false | 
1  | sql复制代码 wait children  | 
Job 的常用函数
这些函数都是线程安全的,所以可以直接在其他
Coroutine中调用。
- fun start(): Boolean
 
调用该函数来启动这个 Coroutine,如果当前 Coroutine 还没有执行调用该函数返回 true,如果当前 Coroutine 已经执行或者已经执行完毕,则调用该函数返回 false
- fun cancel(cause: CancellationException? = null)
 
通过可选的取消原因取消此作业。 原因可以用于指定错误消息或提供有关取消原因的其他详细信息,以进行调试。
- fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
 
通过这个函数可以给 Job 设置一个完成通知,当 Job 执行完成的时候会同步执行这个通知函数。 回调的通知对象类型为:typealias CompletionHandler = (cause: Throwable?) -> Unit. CompletionHandler 参数代表了 Job 是如何执行完成的。 cause 有下面三种情况:
+ 如果 `Job` 是正常执行完成的,则 `cause` 参数为 `null`
+ 如果 `Job` 是正常取消的,则 `cause` 参数为 `CancellationException` 对象。这种情况不应该当做错误处理,这是任务正常取消的情形。所以一般不需要在错误日志中记录这种情况。
+ 其他情况表示 `Job` 执行失败了。这个函数的返回值为 `DisposableHandle` 对象,如果不再需要监控 `Job` 的完成情况了, 则可以调用 `DisposableHandle.dispose` 函数来取消监听。如果 `Job` 已经执行完了, 则无需调用 `dispose` 函数了,会自动取消监听。- suspend fun join()
 
join 函数和前面三个函数不同,这是一个 suspend 函数。所以只能在 Coroutine 内调用。
这个函数会暂停当前所处的 Coroutine直到该Coroutine执行完成。所以 join 函数一般用来在另外一个 Coroutine 中等待 job 执行完成后继续执行。当 Job 执行完成后, job.join 函数恢复,这个时候 job 这个任务已经处于完成状态了,而调用 job.join 的 Coroutine 还继续处于 activie 状态。
请注意,只有在其所有子级都完成后,作业才能完成
该函数的挂起是可以被取消的,并且始终检查调用的Coroutine的Job是否取消。如果在调用此挂起函数或将其挂起时,调用Coroutine的Job被取消或完成,则此函数将引发 CancellationException。
Deferred
1  | kotlin复制代码public interface Deferred<out T> : Job {  | 
通过使用
async创建协程可以得到一个有返回值Deferred,Deferred接口继承自Job接口,额外提供了获取Coroutine返回结果的方法。由于Deferred继承自 Job 接口,所以Job相关的内容在Deferred上也是适用的。 Deferred 提供了额外三个函数来处理和Coroutine执行结果相关的操作。
- suspend fun await(): T
 
用来等待这个Coroutine执行完毕并返回结果。
- fun getCompleted(): T
 
用来获取Coroutine执行的结果。如果Coroutine还没有执行完成则会抛出 IllegalStateException ,如果任务被取消了也会抛出对应的异常。所以在执行这个函数之前,可以通过 isCompleted 来判断一下当前任务是否执行完毕了。
- fun getCompletionExceptionOrNull(): Throwable?
 
获取已完成状态的Coroutine异常信息,如果任务正常执行完成了,则不存在异常信息,返回null。如果还没有处于已完成状态,则调用该函数同样会抛出 IllegalStateException,可以通过 isCompleted 来判断一下当前任务是否执行完毕了。
SupervisorJob
SupervisorJob 是一个顶层函数,定义如下:
1  | kotlin复制代码/**  | 
 该函数创建了一个处于 active 状态的supervisor job。如前所述, Job 是有父子关系的,如果子Job 失败了父Job会自动失败,这种默认的行为可能不是我们期望的。比如在 Activity 中有两个子Job分别获取一篇文章的评论内容和作者信息。如果其中一个失败了,我们并不希望父Job自动取消,这样会导致另外一个子Job也被取消。而SupervisorJob就是这么一个特殊的 Job,里面的子Job不相互影响,一个子Job失败了,不影响其他子Job的执行。SupervisorJob(parent:Job?) 具有一个parent参数,如果指定了这个参数,则所返回的 Job 就是参数 parent 的子Job。如果 Parent Job 失败了或者取消了,则这个 Supervisor Job 也会被取消。当 Supervisor Job 被取消后,所有 Supervisor Job 的子Job也会被取消。
MainScope() 的实现就使用了 SupervisorJob 和一个 Main Dispatcher:
1  | kotlin复制代码/**  | 
- class MyAndroidActivity {
 - private val scope = MainScope()
 - override fun onDestroy() {
 - super.onDestroy()
 - scope.cancel()
 - }
 - }
 1
2
3
4
5
6
7*
* The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
* If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
* `val scope = MainScope() + CoroutineName("MyActivity")`.
*/
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
但是SupervisorJob是很容易被误解的,它和协程异常处理、子协程所属Job类型还有域有很多让人混淆的地方,具体异常处理可以看Google的这一篇文章:协程中的取消和异常 | 异常处理详解
CoroutineDispatcher - 调度器
CoroutineDispatcher定义了 Coroutine 执行的线程。CoroutineDispatcher可以限定Coroutine在某一个线程执行、也可以分配到一个线程池来执行、也可以不限制其执行的线程。
CoroutineDispatcher是一个抽象类,所有dispatcher都应该继承这个类来实现对应的功能。Dispatchers是一个标准库中帮我们封装了切换线程的帮助类,可以简单理解为一个线程池。它的实现如下:
- Dispatchers.Default
 
默认的调度器,适合处理后台计算,是一个CPU密集型任务调度器。如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。注意它和IO共享线程池,只不过限制了最大并发数不同。
- Dispatchers.IO
 
顾名思义这是用来执行阻塞 IO 操作的,是和Default共用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。
- Dispatchers.Unconfined
 
由于Dispatchers.Unconfined未定义线程池,所以执行的时候默认在启动线程。遇到第一个挂起点,之后由调用resume的线程决定恢复协程的线程。
- Dispatchers.Main:
 
指定执行的线程是主线程,在Android上就是UI线程·
由于子Coroutine 会继承父Coroutine 的 context,所以为了方便使用,我们一般会在 父Coroutine 上设定一个 Dispatcher,然后所有 子Coroutine 自动使用这个 Dispatcher。
CoroutineStart - 协程启动模式
- CoroutineStart.DEFAULT:
 
协程创建后立即开始调度,在调度前如果协程被取消,其将直接进入取消响应的状态
虽然是立即调度,但也有可能在执行前被取消
- CoroutineStart.ATOMIC:
 
协程创建后立即开始调度,协程执行到第一个挂起点之前不响应取消
虽然是立即调度,但其将调度和执行两个步骤合二为一了,就像它的名字一样,其保证调度和执行是原子操作,因此协程也一定会执行
- CoroutineStart.LAZY:
 
只要协程被需要时,包括主动调用该协程的start、join或者await等函数时才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态
- CoroutineStart.UNDISPATCHED:
 
协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点
是立即执行,因此协程一定会执行
这些启动模式的设计主要是为了应对某些特殊的场景。业务开发实践中通常使用DEFAULT和LAZY这两个启动模式就够了
CoroutineScope - 协程作用域
定义协程必须指定其
CoroutineScope。CoroutineScope可以对协程进行追踪,即使协程被挂起也是如此。同调度程序 (Dispatcher) 不同,CoroutineScope并不运行协程,它只是确保您不会失去对协程的追踪。为了确保所有的协程都会被追踪,Kotlin不允许在没有使用CoroutineScope的情况下启动新的协程。CoroutineScope可被看作是一个具有超能力的ExecutorService的轻量级版本。CoroutineScope会跟踪所有协程,同样它还可以取消由它所启动的所有协程。这在Android开发中非常有用,比如它能够在用户离开界面时停止执行协程。
Coroutine是轻量级的线程,并不意味着就不消耗系统资源。 当异步操作比较耗时的时候,或者当异步操作出现错误的时候,需要把这个Coroutine取消掉来释放系统资源。在Android环境中,通常每个界面(Activity、Fragment等)启动的Coroutine只在该界面有意义,如果用户在等待Coroutine执行的时候退出了这个界面,则再继续执行这个Coroutine可能是没必要的。另外Coroutine也需要在适当的context中执行,否则会出现错误,比如在非UI线程去访问View。 所以Coroutine在设计的时候,要求在一个范围(Scope)内执行,这样当这个Scope取消的时候,里面所有的子 Coroutine也自动取消。所以要使用Coroutine必须要先创建一个对应的CoroutineScope。
CoroutineScope 接口
1  | kotlin复制代码public interface CoroutineScope {  | 
CoroutineScope 只是定义了一个新 Coroutine 的执行 Scope。每个 coroutine builder 都是 CoroutineScope 的扩展函数,并且自动的继承了当前 Scope 的 coroutineContext 。
分类及行为规则
官方框架在实现复合协程的过程中也提供了作用域,主要用以明确写成之间的父子关系,以及对于取消或者异常处理等方面的传播行为。该作用域包括以下三种:
- 顶级作用域
 
没有父协程的协程所在的作用域为顶级作用域。
- 协同作用域
 
协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。
- 主从作用域
 
与协同作用域在协程的父子关系上一致,区别在于,处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程。
除了三种作用域中提到的行为以外,父子协程之间还存在以下规则:
- 父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。
 - 父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。
 - 子协程会继承父协程的协程上下文中的元素,如果自身有相同
key的成员,则覆盖对应的key,覆盖的效果仅限自身范围内有效。 
常用作用域
官方库给我们提供了一些作用域可以直接来使用,并且 Android 的Lifecycle Ktx库也封装了更好用的作用域,下面看一下各种作用域
GlobalScope - 不推荐使用
1  | kotlin复制代码public object GlobalScope : CoroutineScope {  | 
GlobalScope是一个单例实现,源码十分简单,上下文是EmptyCoroutineContext,是一个空的上下文,切不包含任何Job,该作用域常被拿来做示例代码,由于 GlobalScope 对象没有和应用生命周期组件相关联,需要自己管理 GlobalScope 所创建的 Coroutine,且GlobalScope的生命周期是 process 级别的,所以一般而言我们不推荐使用 GlobalScope 来创建 Coroutine。
runBlocking{} - 主要用于测试
1  | kotlin复制代码/**  | 
这是一个顶层函数,从源码的注释中我们可以得到一些信息,运行一个新的协程并且阻塞当前可中断的线程直至协程执行完成,该函数不应从一个协程中使用,该函数被设计用于桥接普通阻塞代码到以挂起风格(suspending style)编写的库,以用于主函数与测试。该函数主要用于测试,不适用于日常开发,该协程会阻塞当前线程直到协程体执行完成。
MainScope() - 可用于开发
1  | kotlin复制代码/**  | 
- class MyAndroidActivity {
 - private val scope = MainScope()
 - override fun onDestroy() {
 - super.onDestroy()
 - scope.cancel()
 - }
 - }
 1
2
3
4
5
6
7*
* The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
* If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
* `val scope = MainScope() + CoroutineName("MyActivity")`.
*/
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
该函数是一个顶层函数,用于返回一个上下文是SupervisorJob() + Dispatchers.Main的作用域,该作用域常被使用在Activity/Fragment,并且在界面销毁时要调用fun CoroutineScope.cancel(cause: CancellationException? = null)对协程进行取消,这是官方库中可以在开发中使用的一个用于获取作用域的顶层函数,使用示例在官方库的代码注释中已经给出,上面的源码中也有,使用起来也是十分的方便。
LifecycleOwner.lifecycleScope - 推荐使用
1  | kotlin复制代码/**  | 
该扩展属性是 Android 的Lifecycle Ktx库提供的具有生命周期感知的协程作用域,它与LifecycleOwner的Lifecycle绑定,Lifecycle被销毁时,此作用域将被取消。这是在Activity/Fragment中推荐使用的作用域,因为它会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,相同作用的还有下文提到的ViewModel.viewModelScope。
ViewModel.viewModelScope - 推荐使用
1  | kotlin复制代码/**  | 
该扩展属性和上文中提到的LifecycleOwner.lifecycleScope基本一致,它是ViewModel的扩展属性,也是来自Android 的Lifecycle Ktx库,它能够在此ViewModel销毁时自动取消,同样不会造成协程泄漏。该扩展属性返回的作用域的上下文同样是SupervisorJob() + Dispatchers.Main.immediate
coroutineScope & supervisorScope
1  | kotlin复制代码/**  | 
- suspend fun showSomeData() = coroutineScope {
 - val data = async(Dispatchers.IO) { // <- extension on current scope
 - … load some UI data for the Main thread …
 - }
 - withContext(Dispatchers.Main) {
 - doSomeWork()
 - val result = data.await()
 - display(result)
 - }
 - }
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20*
* The scope in this example has the following semantics:
* 1) `showSomeData` returns as soon as the data is loaded and displayed in the UI.
* 2) If `doSomeWork` throws an exception, then the `async` task is cancelled and `showSomeData` rethrows that exception.
* 3) If the outer scope of `showSomeData` is cancelled, both started `async` and `withContext` blocks are cancelled.
* 4) If the `async` block fails, `withContext` will be cancelled.
*
* The method may throw a [CancellationException] if the current job was cancelled externally
* or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope
* (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope).
*/
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
首先这两个函数都是挂起函数,需要运行在协程内或挂起函数内。supervisorScope属于主从作用域,会继承父协程的上下文,它的特点就是子协程的异常不会影响父协程,它的设计应用场景多用于子协程为独立对等的任务实体的时候,比如一个下载器,每一个子协程都是一个下载任务,当一个下载任务异常时,它不应该影响其他的下载任务。coroutineScope和supervisorScope都会返回一个作用域,它俩的差别就是异常传播:coroutineScope 内部的异常会向上传播,子协程未捕获的异常会向上传递给父协程,任何一个子协程异常退出,会导致整体的退出;supervisorScope 内部的异常不会向上传播,一个子协程异常退出,不会影响父协程和兄弟协程的运行。
协程的取消和异常
普通协程如果产生未处理异常会将此异常传播至它的父协程,然后父协程会取消所有的子协程、取消自己、将异常继续向上传递。下面拿一个官方的图来示例这个过程:
这种情况有的时候并不是我们想要的,我们更希望一个协程在产生异常时,不影响其他协程的执行,在上文中我们也提到了一些解决方案,下面我们就在实践一下。
使用SupervisorJob**
在上文中我们也对这个顶层函数做了讲解,那如何使用呢?直接上代码:
1  | kotlin复制代码import androidx.appcompat.app.AppCompatActivity  | 
MainScope()我们之前提到过了,它的实现就是用了SupervisorJob。执行结果就是Child 2抛出异常后,Child 3正常执行了,但是程序崩了,因为我们没有处理这个异常,下面完善一下代码
1  | kotlin复制代码override fun onCreate(savedInstanceState: Bundle?) {  | 
这一次,程序没有崩溃,并且异常处理的打印也输出了,这就达到了我们想要的效果。但是要注意一个事情,这几个子协程的父级是SupervisorJob,但是他们再有子协程的话,他们的子协程的父级就不是SupervisorJob了,所以当它们产生异常时,就不是我们演示的效果了。我们使用一个官方的图来解释这个关系:
这个图可以说是非常直观了,还是官方🐂。新的协程被创建时,会生成新的 Job 实例替代 SupervisorJob。
使用supervisorScope
这个作用域我们上文中也有提到,使用supervisorScope也可以达到我们想要的效果,上代码:
1  | kotlin复制代码import androidx.appcompat.app.AppCompatActivity  | 
可以看到已经达到了我们想要的效果,但是如果将supervisorScope换成coroutineScope,结果就不是这样了。最终还是拿官方的图来展示:
结语
至此文章就已经结束,本文主要是我学习协程的一些记录,分享出来供大家翻阅一下,大家好才是真的好。里面有很多的描述都是摘录自别的文章的,下面也给出了链接,其实官方的文章已经将协程的使用讲的非常全面了,大家可以翻阅一下官方的文章进行学习,虽然可能描述的不是很详细,但是该有的细节都提到了。
后续有时间可能会出一些协程的高级用法的文章,比如协程的冷数据流Flow,这个在我们的项目里也已经用上了,没错,是我引入的😁。总体来说,协程简单使用非常简单,但是想用好,还是需要下一定的功夫去研究的,但是还是逃不过真香定律,大家赶紧学习用起来吧。
我的其他文章:
参考及摘录
掌握Kotlin Coroutine之 Job&Deferred
霍丙乾 - 《深入理解Kotlin协程》
谷歌开发者 - 在 Android 开发中使用协程 | 背景介绍
本文转载自: 掘金