[译]Coroutines的取消和异常处理(Part 1)

351 阅读5分钟

原文作者 :Manuel Vivo

原文地址: Cancellation and Exceptions in Coroutines (Part 1)

译者 : 京平城

Coroutines的取消和异常处理对应用的内存管理以及电量管理是十分重要的;妥善的处理异常对提升用户体验十分关键。
让我们先来了解一下Coroutines的几个核心概念:CoroutineScope,Job和CoroutineContext。

CoroutineScope

我们可以通过调用CoroutineScope的扩展方法launchasync来创建和管理coroutine,并且任何时候都能通过scope.cancel()方法来取消正在运行中的coroutines。

在Android开发中,我们通常会利用KTX扩展包中的一些具有生命周期感知能力(lifecycle)的自定义CoroutineScope,比如viewModelScope和lifecycleScope。

当调用CoroutineScope(context: CoroutineContext)构造方法创建一个CoroutineScope的时候,它接收一个CoroutineContext参数。

// 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
}

译者注: CoroutineScope的构造方法里面的+号一开始会让人觉得不知所云,但它其实是CoroutineContext里的一个操作符重载,如果你使用Android Studio的话,按住ctrl+鼠标左键可以跳转到该操作符重载的具体实现。

Job

Job是coroutine的句柄。CoroutineScope的扩展方法launchasync都会返回一个Job实例,它是一个coroutine的唯一标识并且能够处理coroutine的生命周期。当然你也可以直接传递一个Job实例到CoroutineScope的构造函数中去,然后通过该Job实例来管理coroutine。

译者注: 这里要注意CoroutineScope的构造函数中的Job实例和调用scope.launch返回的Job实例并不是同一个,具体他们之间是什么样的关系呢?我们来看一段代码

val parentJob = Job()
val scope = CoroutineScope(parentJob)

val job = scope.launch {// 或者调用scope.async也是一样的结果
}

val childJob: Job = parentJob.children.iterator().next()

println(childJob === job) //true

证明调用launch方法返回的Job实例,其实就是CoroutineScope构造函数中的那个Job实例的child实例。

如果我们通过launch方法和async方法创建2个不同的coroutines,那么它们返回的Job实例都是CoroutineScope构造函数中的那个Job实例的child实例。

val parentJob = Job()

val scope = CoroutineScope(parentJob + Main)

val job = scope.launch {}

val asyncJob = scope.async {}

val jobIterator = parentJob.children.iterator()

val childJob1: Job = jobIterator.next()

println(childJob1 === job) // true

val childJob2: Job = jobIterator.next()

println(childJob2 === asyncJob) //true

CoroutineContext

CoroutineContext由一系列可以影响coroutine行为的元素(elements)组成:

  • Job — 上面已经介绍过了。
  • CoroutineDispatcher — coroutine的调度器,用来把coroutine派发到不同的线程上去执行。
  • CoroutineName — 给你的coroutine取个名字(类似给Thread取个名字),debugging的时候有用。
  • CoroutineExceptionHandler 异常处理,该系列文章的Part 3会详细介绍。

一个新创建的coroutine的CoroutineContext会自动继承父coroutine的CoroutineContext。
在coroutines内部还能继续创建coroutines,也就是说coroutines是可以嵌套的,并且嵌套的coroutines是有层级关系(父子关系)的。

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()
}

一般来说coroutines层级的根节点都是通过CoroutineScope创建的,其他都是子节点
0_cIlNrjUhl4aZjrkJ.png Coroutines are executed in a task hierarchy. The parent can be either a CoroutineScope or another coroutine.

译者注:就是CoroutineScope构造函数里面传入的那个Job,即使不传的话,该构造函数也会默认生成一个Job实例,scope.cancel()方法内部调用的就是job.cancel()方法

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
// 如果传入的CoroutineContext不包含Job实例,那么默认创建一个Job
    ContextScope(if (context[Job] != null) context else context + Job())
    
public fun CoroutineScope.cancel(cause: CancellationException? = null) {
// scope.cancel()内部调用的就是job.cancel()
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
    job.cancel(cause)
}

Job lifecycle

Job的生命周期分为New, Active, Completing, Completed, Cancelling and Cancelled。我们不能直接访问这些states,但是我们可以访问Job的isActive, isCancelled和isCompleted属性。

0_zGHzocA6-lCxk0aX.png Job lifecycle

一个coroutine执行失败或者被调用了job.cancel()方法,那么它的state会从active变成Cancelling(isActive = false, isCancelled = true)。一旦所有的children都完成了他们的任务,coroutine的state会变成Cancelled,并且此时isCompleted = true。

Parent CoroutineContext explained

coroutine的parent可以是一个CoroutineScope或者另一个coroutine,CoroutineContext的计算公式是

Parent context = Defaults + inherited CoroutineContext + arguments

  • 一些elements是有默认值的,比如CoroutineDispatcher的默认值是Dispatchers.Default,CoroutineName的默认值是“coroutine”。
  • CoroutineContext 可以继承自CoroutineScope或者是创建它的父coroutine。
  • Arguments 当然你可以自己传elements进去覆盖默认的或者是继承自父coroutine的elements。

Note: CoroutineContext使用+操作符来组合elements,+操作符右边的elements会覆盖 +操作符左边的elements,比如 (Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)

0_gjkoETUbx4mPhYEF.png Every coroutine started by this CoroutineScope will have at least those elements in the CoroutineContext. CoroutineName is gray because it comes from the default values.

上述图片中CoroutineScope创建的CoroutineContext为:

New coroutine context = parent CoroutineContext + Job() 此时调用scope的launch方法并传入新的Dispatchers

val job = scope.launch(Dispatchers.IO) {
    // new coroutine
}

现在CoroutineContext中的Dispatchers将从Dispatchers.Main被覆盖成Dispatchers.IO,并且scope.launch(Dispatchers.IO)所返回的Job也将会是一个全新的Job实例(之前我们讲到过其实图中绿色的Job实例和红色的Job实例是父子关系)

0_LPnIOfVGQqrqPZ__.png The Job in the CoroutineContext and in the parent context will never be the same instance as a new coroutine always get a new instance of a Job

此系列文章的Part 3,我们还会介绍不同类型的Job,比如SupervisorJob。
现在有关coroutines的基本概念我们已经学习完了,接着我们在Part 2和Part 3中将继续探讨coroutines的取消和异常处理。