前言
本篇文章是作为我的一个学习记录,写成文章也是为了更好的加深记忆和理解,也是为了分享知识。本文的定位是协程的稍微深入的全面知识,也会示例一些简单的使用,这里不对
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 开发中使用协程 | 背景介绍
本文转载自: 掘金