原文作者 :Manuel Vivo
原文地址: Cancellation and Exceptions in Coroutines (Part 1)
译者 : 京平城
Coroutines的取消和异常处理对应用的内存管理以及电量管理是十分重要的;妥善的处理异常对提升用户体验十分关键。
让我们先来了解一下Coroutines的几个核心概念:CoroutineScope,Job和CoroutineContext。
CoroutineScope
我们可以通过调用CoroutineScope的扩展方法launch和async来创建和管理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的扩展方法launch和async都会返回一个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创建的,其他都是子节点
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属性。
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”)
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实例是父子关系)
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的取消和异常处理。