限定目的,能使人生变得简洁。

协程上下文是个啥?

CoroutineContext,译作“协程上下文”,在协程中是非常重要的概念。你可能会比较好奇,为什么之前都没有注意到它的存在呢?因为协程框架已经为我们包装得非常好了。让我们来看一下launchasync两个函数的签名:

launch

1
2
3
4
5
6
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
onCompletion: CompletionHandler? = null,
block: suspend CoroutineScope.() -> Unit
): Job

async

1
2
3
4
5
6
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
onCompletion: CompletionHandler? = null,
block: suspend CoroutineScope.() -> T
): Deferred<T>

可以看到这两个函数的第一个参数都是CoroutineContext类型的。所有协程构建函数(如launchasync)都是以CoroutineScope的扩展函数的形式被定义的,而CoroutineScope接口唯一的成员就是CoroutineContext类型。

1
2
3
4
5
6
7
8
9
10
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}

协程上下文是协程必备的组成部分,它管理了协程的线程绑定、生命周期、异常处理和调试功能,接下来我们分析上下文具体的结构组成。

协程上下文的结构

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
2
3
typealias CoroutineContext = Map<CoroutineContext.Key<*>, CoroutineContext.Element>

fun get(key: CoroutineContext.Key<*>): CoroutineContext.Element?

如果使用这种实现,我们每次调用get之后,必须用显式的类型转换,才能得到想要的Element类型。而CoroutineContext则通过泛型(CoroutineContext的Key即带有类型信息)为我们解决了类型转换的痛点:

1
fun <E : Element> get(key: Key<E>): E?

在CoroutineContext上可进行的操作

CoroutineContext并未实现标准的集合接口,因此无法使用iterator()等集合标准操作。它有独特的一套操作。

“拼装”上下文对象

对CoroutineContext来说,最重要的操作是plusplus操作用于把两个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

如果你在构建协程时使用了无法传递异常的构建器,如launchactor,当异常发生时,需要有一个异常处理器来处理它。CoroutineExceptionHandler就是充当这样的异常处理器。

名字:CoroutineName

协程的别名,一般是用于调试,以区分多个协程。

上述Element内部都以伴生对象的形式定义了相应的Key,可以通过coroutineContext[element type name]的形式方便地获取到Element对象。比如coroutineContext[Job]会返回Job或者null(如果没有Job)。

协程构建过程中的CoroutineContext

前面讲过CoroutineScope实际上是一个CoroutineContext的封装,当我们需要启动一个协程时,会在CoroutineScope的实例上调用构建函数,如asynclaunch。在构建函数中,一共出现了3个CoroutineContext:

  • inherited context:从CoroutineScope中继承得到的上下文对象
  • context argument:构建函数中传入的第一个参数,默认为EmptyCoroutineContext
  • coroutine context:挂起代码块(lambda函数)运行时的上下文对象

如果我们查看协程构建函数asynclaunch的源码,会发现它们第一行都是如下代码:

1
val newContext = newCoroutineContext(context)

再进一步查看:

1
2
3
4
5
6
7
8
// CoroutineContext.kt
@ExperimentalCoroutinesApi
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
val combined = coroutineContext + context
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
}

这里就比较清晰了:构建器函数内部进行了一个CoroutineContext拼接操作,plus的左值是CoroutineScope内部的CoroutineContext,右值是作为构建函数参数的CoroutineContext。根据我们前面讲到的拼接操作,左值具有更高的优先级。

此外,抽象类AbstractCoroutineScope实现了CoroutineScopeJob接口,大部分CoroutineScope的实现都继承自AbstractCoroutineScope,意味着他们同时也是一个Job。可以得到:coroutine context = parent context + coroutine job

Elements默认值

对于上述4个Elements,如果既没有显示指明,则会取相应的默认值:

  • ContinuationInterceptor:默认值为Dispatchers.Default,基于线程池实现,线程数目=CPU数目,且最少为2支
  • Job:默认值为null,在这种情况下,协程是孤儿(没有父协程,无法被父协程取消,例如GlobalScope
  • CoroutineExceptionHandler:它的情况比较复杂,当异常发生时,若没有指定CoroutineExceptionHandler,会使用全局的异常处理器,在全局异常处理器中调用当前线程的uncaughtExceptionHandler。代码如下:
1
2
3
4
5
6
7
8
9
10
// CoroutineExceptionHandlerImpl.kt
internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
// use additional extension handlers
for (handler in handlers) {
handler.handleException(context, exception)
}
// use thread's handler
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}
  • Name:默认为“coroutine”

对于上述默认值,用代码实现起来也并不复杂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
val defaultExceptionHandler = CoroutineExceptionHandler { ctx, t ->
ServiceLoader.load(
serviceClass,
serviceClass.classLoader
).forEach{
it.handleException(ctx, t)
}
Thread.currentThread().let {
it.uncaughtExceptionHandler.uncaughtException(it, exception)
}
}
class CoroutineContext(
val continuationInterceptor: ContinuationInterceptor =
Dispatchers.Default,
val parentJob: Job? =
null,
val coroutineExceptionHandler: CoroutineExceptionHandler =
defaultExceptionHandler,
val name: CoroutineName =
CoroutineName("coroutine")
)

用例浅析

上述4个Elements中,最重要的是Dispatcher和Job两个,我们来看一些例子。

Global Scope Context

1
2
3
GlobalScope.launch {
/* ... */
}

全局Scope使用全默认的4个Elements,意味着它使用Dispatchers.Default和为空的Job(无法通过父Job取消)。

Fully Qualified Context

1
2
3
4
5
6
7
8
launch(
Dispatchers.Main +
Job() +
CoroutineName("HelloCoroutine") +
CoroutineExceptionHandler { _, _ -> /* ... */ }
) {
/* ... */
}

全限定Context,即全部显式指定具体值的Elements。不论你用哪一个CoroutineScope构建该协程,它都具有一致的表现,不会受到CoroutineScoipe任何影响。

CoroutineScope Context

这里我们基于Activity生命周期实现一个CoroutineScope:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class ScopedAppActivity:
AppCompatActivity(),
CoroutineScope
{
protected lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main // 注意这里使用+拼接CoroutineContext

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}

override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
  • Dispatcher:使用Dispatcher.Main,以在UI线程进行绘制
  • Job:在onCreate时构建,在onDestroy时销毁,所有基于该CoroutineContext创建的协程,都会在Activity销毁时取消,从而避免Activity泄露的问题

临时指定参数

前面数次提到过,CoroutineContext的参数主要有两个来源:从scope中继承+参数指定。我们可以用withContext便捷地指定某个参数启动子协程,例如我们想要在协程内部执行一个无法被取消的子协程:

1
2
3
withContext(NonCancellable) {
/* ... */
}

读取协程上下文参数

可以通过顶级挂起只读属性coroutineContext获取协程上下文参数,它位于 kotlin-stdlib / kotlin.coroutines / coroutineContext

1
println("Running in ${coroutineContext[CoroutineName]}")

Nested Context

内嵌上下文切换是指:在协程A内部构建协程B时,B会自动继承A的Dispatcher,如果没有注意这一点,很容易发生诸如“主线程执行耗时操作”的错误。

我们可以在调用async时加入Dispatcher参数,以切换到工作线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误的做法,在主线程中直接调用async,若耗时过长则阻塞UI
GlobalScope.launch(Dispatchers.Main) {
val deferred = async {
/* ... */
}
/* ... */
}

// 正确的做法,在工作线程执行协程任务
GlobalScope.launch(Dispatchers.Main) {
val deferred = async(Dispatchers.Default) {
/* ... */
}
/* ... */
}

小结

  • 协程上下文环境参数可以用加号+拼接,左值优先
  • 上下文环境可以继承
  • 上下文环境可以单独制定参数

参考

本文的70%都翻译总结自Medium上的这篇文章,写的非常棒,建议有英文阅读能力的同学直接阅读原文