原文:https://medium.com/androiddevelopers/coroutines-first-things-first-e6187bf3bb21
本系列博客文章深入探讨了协程中的取消和异常。取消对于避免做多余的工作很重要,这会浪费内存和电量;正确的异常处理是良好用户体验的关键。作为本系列其他 3 部分(第 2 部分:取消,第 3 部分:异常,第 4 部分:不应取消的工作的协程和模式)的基础,定义一些核心协程概念非常重要,例如 CoroutineScope, Job 和 CoroutineContext 以便我们都在同一页面上。
CoroutineScope 协程作用域
一个 CoroutineScope 跟踪您使用 launch 或 async 创建的任何协程(这些是 CoroutineScope 上的扩展函数)。可以通过 scope.cancel() 在任何时间点调用来取消正在进行的工作(正在运行的协程)。
每当您想启动和控制应用程序特定层中协程的生命周期时,您都应该创建一个 CoroutineScope。在一些像 Android 这样的平台上,有一些 KTX 库已经在某些生命周期类中提供了一个 CoroutineScope,比如 viewModelScope 和 lifecycleScope。
创建一个 CoroutineScope 时,它将一个 CoroutineContext 作为其构造函数的参数。您可以使用以下代码创建一个新的 scope 作用域和 coroutine 协程:
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
// new coroutine
}
Job 工作任务
一个 Job 是协程的句柄。对于您(通过 launch 或 async)创建的每个协程,它返回一个唯一标识协程并管理其生命周期的 Job 实例。正如我们在上面看到的,您还可以将一个 Job 传递给一个 CoroutineScope 以保持对其生命周期的句柄。
CoroutineContext 协程上下文
CoroutineContext 是一组定义协程行为的元素。 它由以下组成:
- Job — 控制协程的生命周期。
- CoroutineDispatcher — 将工作分派到适当的线程。
- CoroutineName — 协程的名称,用于调试。
- CoroutineExceptionHandler — 处理未捕获的异常,将在本系列的第 3 部分中介绍。
什么是新的协程的 CoroutineContext ?我们已经知道将创建一个新 Job 实例,允许我们控制它的生命周期。其余元素将继承其父级(另一个协程或创建它的 CoroutineScope)的 CoroutineContext。
由于一个 CoroutineScope 可以创建协程并且您可以在协程内创建更多协程,因此创建了隐式任务层次结构。在下面的代码片段中,除了使用 CoroutineScope 来创建一个新的协程,看看如何在一个协程中创建更多的协程:
val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {
// New coroutine that has CoroutineScope as a parent
val result = async {
// New coroutine that has the coroutine started by
// launch as a parent
}.await()
}
该层次结构的根通常是 CoroutineScope。我们可以将层次结构可视化如下:协程在任务层次结构中执行。父级可以是一个 CoroutineScope 或另一个协程。
Job 生命周期
一个 Job 可以经历一组状态:New、Active、Completing、Completed、Canceling 和 Cancelled。虽然我们没有权限访问状态本身,我们可以访问 Job 的属性:isActive,isCancelled 和 isCompleted。作业生命周期
如果协程处于活动状态,则协程的失败或 job.cancel() 调用会将作业移至取消状态 ( isActive = false, isCancelled = true)。一旦所有孩子都完成了他们的工作,协程将进入 Canceled 状态并且 isCompleted = true。
父 CoroutineContext 说明
在任务层次结构中,每个协程都有一个父协程,可以是一个 CoroutineScope 协程或另一个协程。但是,一个协程的 CoroutineContext 父级可能与父级(协程)的 CoroutineContext 的不同,因为它是根据以下公式计算的:
Parent context = Defaults + inherited CoroutineContext + arguments
在这里:
- 某些元素具有默认值:Dispatchers.Default 是 CoroutineDispatcher 的默认值,“coroutine” 是 CoroutineName 的默认值。
- 继承的 CoroutineContext 是 CoroutineScope 的或者创建它的协程的 CoroutineContext。
- 在协程构建器中传递的参数将优先于继承上下文中的那些元素。
注意:CoroutineContext 可以使用+运算符组合 。由于 CoroutineContext 是一组元素,因此将使用加号右侧的元素覆盖左侧的元素来创建一个新的 CoroutineContext。例如
(Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)
这个 CoroutineScope 启动的每个协程至少在 CoroutineContext 中有这些元素。CoroutineName 是灰色的,因为它来自默认值。
现在我们知道一个新协程的父级 CoroutineContext 是什么,它的实际 CoroutineContext 将是:
New coroutine context = parent CoroutineContext + Job()
如果以如上图所示的 CoroutineScope,我们像如下创建一个新的协程:
val job = scope.launch(Dispatchers.IO) {
// new coroutine
}
该协程的父级 CoroutineContext 及其实际的 CoroutineContext 是什么?请参阅下图中的解决方案!CoroutineContext 中的和父级 CoroutineContext 中的 Job 永远不会是同一个实例,因为新的协程总是得到一个 Job 的新实例
父级 CoroutineContext 有 Dispatchers.IO 而不是 scope 的 CoroutineDispatcher,因为它被协程构建器的参数覆盖。此外,检查父级 CoroutineContext 中的 Job 是 scope 的 Job(红色)的实例,并且一个新的 Job(绿色)实例已分配给新协程的实际 CoroutineContext。
正如我们将在本系列的第 3 部分中看到的, 一个 CoroutineScope 在它的 CoroutineContext 中可以有一个不同的 Job 实现,称作 SupervisorJob,它改变了 CoroutineScope 处理异常的方式。因此,使用该 scope 创建的新协程可以以 SupervisorJob 作为父级 Job。但是,当一个协程的父协程是另一个协程时,父级 Job 将始终是 Job 类型。