CoroutineScope(协程作用域)
CoroutineScope 可以帮助你追踪任何通过 launch 和 async 启动的协程。它们都是 CoroutineScope 的扩展函数。正在运行的协程可以通过调用 scope.cancel() 在任意时间点停止。
无论你在 App 的任何页面启动协程,并控制其生命周期,都应该创建 CoroutineScope 。在 Android 中,KTX 类库已经为特定的生命周期类提供了 CoroutineScope,例如 viewModelScope 和 lifecycleScope 。
创建 CoroutineScope 时需要给构造函数提供 CoroutineContext(协程上下文) 参数。下面的代码演示了如何新建一个作用域和协程。
// Job 和 Dispatcher 合并在一起作为 CoroutineContext,稍后会进行说明 val scope = CoroutineScope(Job() + Dispatchers.Main) val job = scope.launch { // 新协程 } 复制代码
Job
Job 代表了一个协程。通过 launch 和 async 启动的每一个协程,都会返回一个 Job 实例来唯一标识,并且管理该协程的生命周期。如上一节所示,你可以给 CoroutineScope 传递一个 Job 来控制它的生命周期。
CoroutineContext(协程上下文)
可以翻译成协程上下文。但我还是用英文吧。
CoroutineContext 是定义协程行为的一系列元素。它由以下几部分组成:
- Job,管理协程的生命周期
- CoroutineDispatcher,分发任务到合适的线程
- CoroutineName,协程的名称,用于调试
- CoroutineExceptionHandler,处理未捕获的异常,这是第三篇文章的内容
一个新协程的 CoroutineContext 是什么?我们已经知道会创建一个新的 Job 来帮助我们管理生命周期,剩下的元素将继承自它的父亲的 CoroutineContext (可能是另一个协程,或者是创建它的 CoroutineScope)。
由于 CoroutineScope 可以创建协程,并且你可以在一个协程内部创建多个协程。这就形成了一个隐式的层级结构。在下面的代码中,除了使用 CoroutineScope 创建新协程之外,还展示了如何在一个协程中创建多个协程。
val scope = CoroutineScope(Job() + Dispatchers.Main) val job = scope.launch { // 这里的新协程的父亲是 scope val result = async { // 这里的新协程的父亲是上面的 scope.launch 启动的协程 }.await() } 复制代码
层级结构的根通常是 CoroutineScope 。我们可以把层级结构想象成下面这样:
Job 生命周期
Job 会经历以下生命周期:
New, Active, Completing, Completed, Cancelling , Cancelled
通过 Job 的这几个属性可以获取它的状态:isActive、isCancelled 和 isCompleted 。
Job lifecycle
当协程处于 Active 状态,失败或者取消都会让协程移动到 Cancelling 状态(isActive = false, isCancelled = true)。当协程中的所有子协程都完成了任务,协程将会进入 Cancelled 状态 (isCompleted = true) 。
关于 Parent CoroutineContext
在协程的继承结构中,每一个协程都会有一个父亲,这个父亲可能是 CoroutineScope 或者另一个协程。 但是子协程最终的父 CoroutineContext 可能和其父亲原本的 CoroutineContext 不一样。
父 CoroutineContext 的计算公式如下:
Parent context = Defaults + 继承的 CoroutineContext + arguments
其中:
- 其中一些元素具有默认值:
CoroutineDispatcher的默认值是Dispatchers.Default,CoroutineName的默认值是coroutine - 继承的
CoroutineContext是父亲的 CoroutineContext - 传递到协程构建器中的参数优先于继承自上下文的参数
注意:多个 CoroutineContext 可以通过 “+” 操作符合并。由于 CoroutineContext 包含一系列元素,当创建新的 CoroutineContext 时,“+” 右侧的元素将会覆盖左侧的元素。例如:(Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”) 。
通过此协程作用域创建的协程的 CoroutineContext 将至少包含上图中这些元素。CoroutineName 是灰色的,因为它是默认值。
现在我们知道一个新协程的父 CoroutineContext 是什么了。它自己的 CoroutineContext 实际上是这样的:
New coroutine context = parent CoroutineContext + Job()
通常上面的协程作用域创建一个新的协程:
val job = scope.launch(Dispatchers.IO) { //新协程 } 复制代码
那么它的父 CoroutineContext 和自己的 CoroutineContext 是什么样的呢?请看下面的图片。
注意上下两个 Job 并不是同一个实例,新协程总会得到一个新的 Job 实例。
最终的父 CoroutineContext 的协程调度器是 Dispatchers.IO,因为它被协程构建器中的参数覆盖了。(译者注:scope.launch(Dispatchers.IO)) 。
同时,注意父 CoroutineContext 中的 Job 实例就是 scope 的 Job 实例(红色),而传递到新协程的 CoroutineContext 中的 Job 是一个新的实例(绿色)。
在系列第三篇文章中我们将看到,CoroutineScope 可以拥有其他的 Job 实现类,SupervisorJob ,它会改变协程作用域的异常处理。因此,在这样的 CoroutineScope 中创建的子协程也将继承 SupervisorJob 类型的 Job 。但是,如果当父协程是另一个协程的时候,将总是 Job 类型。
现在你已经了解了协程的基础知识,在系列的后面两篇文章中学习更多 取消和异常 的知识吧!



