我将翻译三篇介绍协程的 取消 和 异常处理 相关的文章,三篇文章是层层递进的关系。翻译过程中我将尽量忠实于原文。当然,由于水平有限,不能保证完全的翻译正确。如果您发现翻译有错误之处,欢迎在评论里指出。我也将贴出每篇翻译的原文。
- 第一篇:《协程:第一件事》(原文: Coroutines: first things first)
- 第二篇:《协程的取消》(原文:Cancellation in coroutines)
- 第三篇:《协程的异常》(原文:Exceptions in coroutines)
那么,这是第一篇。
本系列的博文将深入讨论协程的 取消(cancellation
)和 异常(exception
)。取消是很重要的,可以避免做不必要的工作,以避免浪费内存和电量;适当的异常处理是用户体验的关键所在。作为该系列其它篇章的基础,定义好协程的一些核心概念非常重要,比如 CoroutineScope
,Job
和 CoroutineContext
,以便我们对这些概念有一致的认知。
如果您更喜欢视频,可以查看2019年Kotlin大会上的演讲视频:
KotlinConf 2019: Coroutines! Gotta catch 'em all! by Florina Muntenescu & Manuel Vivo
CoroutineScope
CoroutineScope
能跟踪使用 launch
或 async
方法创建的任意协程(这两个方法是 CoroutineScope
的扩展方法)。在任意时间点,调用 scope.cancel()
方法都可以取消正在执行的协程。
无论何时,当你想在应用的特定层级启动一个协程并且控制它的生命周期,你就应该创建一个 CoroutineScope
。在某些平台上,比如Android,就已经有 KTX
库可以在特定的生命周期类中提供 CoroutineScope
,比如 viewModelScope
和 lifecycleScope
。
创建一个 CoroutineScope
需要 CoroutineContext
作为它构造函数的参数。你可以通过下面的代码创建一个新的 scope
和协程:
// Job 和 Dispatcher 被组合成一个 CoroutineContext,我们后面会讨论到
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
//新的协程
}
Job
Job
是协程的一个句柄。所有你通过 launch
或 async
创建的协程都会返回一个 Job
实例,它可以标识该协程,并且能控制协程的生命周期。在上面的代码中我们可以看到,你可以传递一个 Job
给 CoroutineScope
来保持对其生命周期的控制。
CoroutineContext
CoroutineScope
是由一系列元素组合而成的,用来定义协程的行为。它的组成元素包括:
Job
—— 控制协程的生命周期CoroutineDispatcher
—— 线程调度器CoroutineName
—— 协程的名字,主要用于调试CoroutineExceptionHandler
—— 处理未被捕获的异常,我们将在该系列的第三篇中讲到它
一个新协程的 CoroutineContext
是什么样子的?我们已经知道它将创建一个新的 Job
实例,用于控制其生命周期。它其余的元素将从它的父节点(创建它的另一个协程,或 CoroutineScope
)继承而来。
由于 CoroutineScope
可以创建协程,并且你可以在一个协程内部创建更多的协程,所以就构建了一个隐式的任务层次结构。在下面的代码片段中,使用 CoroutineScope
,除了创建一个新的协程之外,还可以看到在协程内部如何创建更多的协程:
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
//scope作为父节点的新协程
val result = async {
//由launch创建的协程作为父节点的新协程
}.await()
}
该层次结构的根节点通常是 CoroutineScope
,我们把它可视化如下:
(协程在一个任务层次结构中执行,其父节点可以是一个 CoroutineScope
,也可以是另一个协程)
Job的生命周期
一个 Job
可以经历一系列状态:New
,Active
,Completing
,Completed
,Cancelling
以及 Cancelled
。我们无法直接访问 Job
的状态,但可以访问它的属性:isActive
,isCancelled
以及 isCompleted
。它们之间的关系如下图:
如果一个协程处于 Active
的状态,当协程发生异常或调用了 job.cancel()
方法,其 Job
的状态将变为 Cancelling
(isActive = false, isCancelled = true
)。一旦所有子协程的任务都已完成,该协程就会走到 Cancelled
状态,并且 isCompleted = true
。
Parent CoroutineContext
在任务层次结构中,每一个协程都有一个父节点,父节点可能是一个 CoroutineScope
,也可能是另一个协程。然而,子协程的 parent CoroutineContext
和其父节点的 CoroutineContext
可能不一样,它由以下公式计算而成:
Parent context = 默认值 + 继承的CoroutineContext + 传参
- 一些元素有默认的值,比如
CoroutineDispatcher
的默认值是Dispatchers.Default
,CoroutineName
的默认值是"coroutine"
。 - 继承的
CoroutineContext
就是创建了该协程的CoroutineScope
或 另一个协程 的CoroutineContext
。 - 传给协程构建器的参数优先于来自继承的参数。
注意:CoroutineContext
可以使用 +
操作符来组合。由于 CoroutineContext
是元素的组合,一个新的 CoroutineContext
被创建的时候,加号操作符右侧的元素会覆盖操作符左侧的元素。
比如: (Dispatchers.Main
, "name"
) + (Dispatchers.IO
) = (Dispatchers.IO
, "name"
)
(每一个由图片中的这个 CoroutineScope
启动的协程,其 CoroutineContext
里都至少有这些元素。CoroutineName
是灰色的因为它使用了默认值。)
现在我们知道了什么是一个新协程的 parent CoroutineContext
,而新协程自身的 CoroutineContext
将是:
新协程的CoroutineContext = parent CoroutineContext + Job()
如果我们用上图中的 CoroutineScope
创建一个新的协程,如下:
val job = scope.launch(Dispatchers.IO) {
//新协程
}
那么它的 parent CoroutineContext
和它自身的 CoroutineContext
是怎样的呢?从下图中可以看到答案!
(协程 CoroutineContext
中的 Job
和其 parent CoroutineContext
中的 Job
永远不会是同一个实例,一个新的协程总会获取一个新的 Job
实例)
新建协程的 parent CoroutineContext
使用了 Dispatchers.IO
,而不是 scope
中的 CoroutineDispatcher
,因为它被协程构建器中的参数覆盖了。并且,parent CoroutineContext
中的 Job
就是 scope
的 Job
实例(红色),新协程的 CoroutineContext
中的 Job
是一个新的实例(绿色)。
在该系列文章的第三篇中,我们将看到 CoroutineScope
的 CoroutineContext
中可以有一个不同的 Job
实现叫做 SupervisorJob
,它将改变 CoroutineScope
处理异常的方式。因此,一个使用这种 scope
创建的新协程将以 SupervisorJob
作为 parent Job
。然而,当一个协程的父节点是另一个协程时, parent Job
的类型总是 Job
(而非 SupervisorJob
)。