限定目的,能使人生变得简洁。
协程上下文是个啥?
CoroutineContext,译作“协程上下文”,在协程中是非常重要的概念。你可能会比较好奇,为什么之前都没有注意到它的存在呢?因为协程框架已经为我们包装得非常好了。让我们来看一下launch
和async
两个函数的签名:
launch
1 | fun CoroutineScope.launch( |
async
1 | fun <T> CoroutineScope.async( |
可以看到这两个函数的第一个参数都是CoroutineContext
类型的。所有协程构建函数(如launch
和async
)都是以CoroutineScope
的扩展函数的形式被定义的,而CoroutineScope
接口唯一的成员就是CoroutineContext
类型。
1 | public interface CoroutineScope { |
协程上下文是协程必备的组成部分,它管理了协程的线程绑定、生命周期、异常处理和调试功能,接下来我们分析上下文具体的结构组成。
协程上下文的结构
It is an indexed set of Element instances. An indexed set is a mix between a set and a map. Every element in this set has a unique Key. Keys are compared by reference.
CoroutineContext接口跟Map很类似,具有如下特点:
- 有序Map
- Key唯一
- 类型安全
##跟Map类似,为什么不直接用Map
那么我们为什么不直接用Map来实现呢?参考下面一段代码,它用Map实现了类似CoroutineContext的功能
1 | typealias CoroutineContext = Map<CoroutineContext.Key<*>, CoroutineContext.Element> |
如果使用这种实现,我们每次调用get
之后,必须用显式的类型转换,才能得到想要的Element
类型。而CoroutineContext则通过泛型(CoroutineContext的Key即带有类型信息)为我们解决了类型转换的痛点:
1 | fun <E : Element> get(key: Key<E>): E? |
在CoroutineContext上可进行的操作
CoroutineContext并未实现标准的集合接口,因此无法使用iterator()
等集合标准操作。它有独特的一套操作。
“拼装”上下文对象
对CoroutineContext来说,最重要的操作是plus
,plus
操作用于把两个CoroutineContext对象合并成一个。合并时有一个优先级规则:plus
右侧对象的属性会覆盖左侧对象中的同名属性。
[The plus operator] returns a context containing elements from this context and elements from other context. The elements from this context with the same key as in the other one are dropped.
Element即是CoroutineContext
我们知道CoroutineContext中的Value是Element,其实Element本身也是继承于CoroutineContext。这样做的好处是,当我们只有一个CoroutineContext.Element对象时,也可以把它作为一个CoroutineContext来使用,这种一般称之为singleton context。
空对象
除了singleton context,还有一种特殊的上下文对象,EmptyCoroutineContext
。它不含有任何Element,因此,当使用plus
连接符连接一个EmptyCoroutineContext
和另一个上下文对象时,总是得到与另一个上下文对象相同的对象。
认识一下那些Elements
如果我们想要查看CoroutineContext里都可以包含哪些Elements,可以搜索CoroutineContext.Key接口的实现,因为CoroutineContext是一个保存类型确定元素的Map。经过搜索后,我们发现以下几个典型Element:
指定执行线程:ContinuationInterceptor
用于处理协程挂载在线程上的逻辑,抽象类CoroutineDispatcher实现了该接口,一般常用的Dispatcher都会继承于CoroutineDispatcher。
层级关系管理:Job
用于管理任务层级,处理任务父子关系。
- 手动终止父Job时,其中的子Job也被终止
- 当所有子Job运行结束时,父Job才可以运行结束
处理异常:CoroutineExceptionHandler
如果你在构建协程时使用了无法传递异常的构建器,如launch
和actor
,当异常发生时,需要有一个异常处理器来处理它。CoroutineExceptionHandler
就是充当这样的异常处理器。
名字:CoroutineName
协程的别名,一般是用于调试,以区分多个协程。
上述Element内部都以伴生对象的形式定义了相应的Key,可以通过coroutineContext[element type name]
的形式方便地获取到Element对象。比如coroutineContext[Job]
会返回Job或者null(如果没有Job)。
协程构建过程中的CoroutineContext
前面讲过CoroutineScope
实际上是一个CoroutineContext
的封装,当我们需要启动一个协程时,会在CoroutineScope
的实例上调用构建函数,如async
和launch
。在构建函数中,一共出现了3个CoroutineContext:
- inherited context:从
CoroutineScope
中继承得到的上下文对象 - context argument:构建函数中传入的第一个参数,默认为
EmptyCoroutineContext
- coroutine context:挂起代码块(lambda函数)运行时的上下文对象
如果我们查看协程构建函数async
和launch
的源码,会发现它们第一行都是如下代码:
1 | val newContext = newCoroutineContext(context) |
再进一步查看:
1 | // CoroutineContext.kt |
这里就比较清晰了:构建器函数内部进行了一个CoroutineContext拼接操作,plus的左值是CoroutineScope
内部的CoroutineContext
,右值是作为构建函数参数的CoroutineContext
。根据我们前面讲到的拼接操作,左值具有更高的优先级。
此外,抽象类AbstractCoroutineScope
实现了CoroutineScope
和Job
接口,大部分CoroutineScope的实现都继承自AbstractCoroutineScope
,意味着他们同时也是一个Job
。可以得到:coroutine context = parent context + coroutine job。
Elements默认值
对于上述4个Elements,如果既没有显示指明,则会取相应的默认值:
- ContinuationInterceptor:默认值为
Dispatchers.Default
,基于线程池实现,线程数目=CPU数目,且最少为2支 - Job:默认值为null,在这种情况下,协程是孤儿(没有父协程,无法被父协程取消,例如
GlobalScope
) - CoroutineExceptionHandler:它的情况比较复杂,当异常发生时,若没有指定
CoroutineExceptionHandler
,会使用全局的异常处理器,在全局异常处理器中调用当前线程的uncaughtExceptionHandler
。代码如下:
1 | // CoroutineExceptionHandlerImpl.kt |
- Name:默认为“coroutine”
对于上述默认值,用代码实现起来也并不复杂:
1 | val defaultExceptionHandler = CoroutineExceptionHandler { ctx, t -> |
用例浅析
上述4个Elements中,最重要的是Dispatcher和Job两个,我们来看一些例子。
Global Scope Context
1 | GlobalScope.launch { |
全局Scope使用全默认的4个Elements,意味着它使用Dispatchers.Default
和为空的Job
(无法通过父Job取消)。
Fully Qualified Context
1 | launch( |
全限定Context,即全部显式指定具体值的Elements。不论你用哪一个CoroutineScope
构建该协程,它都具有一致的表现,不会受到CoroutineScoipe
任何影响。
CoroutineScope Context
这里我们基于Activity生命周期实现一个CoroutineScope:
1 | abstract class ScopedAppActivity: |
- Dispatcher:使用
Dispatcher.Main
,以在UI线程进行绘制 - Job:在
onCreate
时构建,在onDestroy
时销毁,所有基于该CoroutineContext创建的协程,都会在Activity销毁时取消,从而避免Activity泄露的问题
临时指定参数
前面数次提到过,CoroutineContext的参数主要有两个来源:从scope中继承+参数指定。我们可以用withContext
便捷地指定某个参数启动子协程,例如我们想要在协程内部执行一个无法被取消的子协程:
1 | withContext(NonCancellable) { |
读取协程上下文参数
可以通过顶级挂起只读属性coroutineContext
获取协程上下文参数,它位于 kotlin-stdlib / kotlin.coroutines / coroutineContext:
1 | println("Running in ${coroutineContext[CoroutineName]}") |
Nested Context
内嵌上下文切换是指:在协程A内部构建协程B时,B会自动继承A的Dispatcher,如果没有注意这一点,很容易发生诸如“主线程执行耗时操作”的错误。
我们可以在调用async
时加入Dispatcher参数,以切换到工作线程。
1 | // 错误的做法,在主线程中直接调用async,若耗时过长则阻塞UI |
小结
- 协程上下文环境参数可以用加号
+
拼接,左值优先 - 上下文环境可以继承
- 上下文环境可以单独制定参数
参考
本文的70%都翻译总结自Medium上的这篇文章,写的非常棒,建议有英文阅读能力的同学直接阅读原文