返回

CoroutineContext的作用

2023-03-10 by liujian

首先

先说总结,CoroutineContext的作用是为协程存储各种类信息,本质上是一种集合(可以理解为一种Map);存储的这些类都是CoroutineContext的具体子类,他们都有各自的作用,在使用时直接通过CoroutineContext的get方法取出,非常方便;

CoroutineContext的源码在CoroutineContext.kt文件中,代码很短,但是细节需要特别注意。先看类说明:

1
Persistent context for the coroutine. 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.

翻译下:
为协程提供上下文。它是Element实例的索引集。索引集是集和映射之间的混合体。这个集合中的每个元素都有一个唯一的Key。

在此建议大家先阅读CoroutineContext源码,再看下文章 Kotlin协程源码分析-7 Context左向链表

先将文章中的示例代码拿出来,方便大家后续查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class My3CoroutineName(
val name: String
) : AbstractCoroutineContextElement(My3CoroutineName) {
public companion object Key : CoroutineContext.Key<My3CoroutineName>

override fun toString(): String = "CoroutineName($name)"
}

public class My4CoroutineName(
val name: String
) : AbstractCoroutineContextElement(My4CoroutineName) {

public companion object Key : CoroutineContext.Key<My4CoroutineName>

override fun toString(): String = "CoroutineName($name)"
}

这段代码中有一个特别需要说明的点就是My3CoroutineName继承了类AbstractCoroutineContextElement,该类定义如下

1
public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element

构造函数传递的参数类型是key类型,而我们传递的是My3CoroutineName,貌似错误但却能正常运行,因为这里用到了Kotlin的语法糖;此处的My3CoroutineName实际指向的是该类的伴生对象Key;这个用法在协程中使用广泛。

其次

文章Kotlin协程源码分析-7 Context左向链表 中的给出的示例对于大家理解左向链表有着非常大的帮助,在此额外补充个示例,小伙伴可以自己先思考下,再比对下结果是否与自己想的一致;

1
2
3
4
5
6
7
8
9
fun studyContext4() {
val my3CoroutineName = My3CoroutineName("item3")
val my4CoroutineName = My4CoroutineName("item4")
val my5CoroutineName = My3CoroutineName("item5")

val newElement = (my3CoroutineName + my4CoroutineName) + my5CoroutineName

println("(3+4)+5:$newElement")
}

输出如下

1
(3+4)+5:[CoroutineName(item4), CoroutineName(item5)]

我们发现item3被移除掉了,本质原因在文章开头说过了,这个集合中的每个元素都有一个唯一的Keyitem3item5虽然是2个对象,但是他们具有相同的Key,所以在执行plus操作时,原先的item3会被移除掉,结果就是item4item5

最后

使用一个实际工作的例子来结束这篇文章。如果之前没有用过异常处理器的小伙伴,建议阅读Kotlin 协程的异常处理Kotlin协程核心库分析-5 Job异常处理器注意点

实际开发工作中,我们有时需要对协程可能出现的异常增加try catch处理逻辑,但是每次编写这样的样板代码,浪费时间且毫无营养,于是就想着做个包装方法处理这段逻辑;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
fun <T> CoroutineScope.launchWithCatch(
context: CoroutineContext = EmptyCoroutineContext,
onComplete: ((Result<T>) -> Unit)? = null,
onCancel: (() -> Unit)? = null,
errorHandler: ((exception: Throwable) -> Unit)? = null,
onFinally:((Result<T>) -> Unit)? = null,
block: suspend CoroutineScope.() -> T
): Job {
val ref = AtomicReference<CoroutineContext>(null)
//由于当前的CoroutineScope不一定是根协程;因此必须加入SupervisorJob;否则可能出现CoroutineExceptionHandler无法捕获的bug请注意
return this.launch(context + SupervisorJob() + CoroutineExceptionHandler { coroutineContext, throwable ->
//只有当coroutineContext是根coroutineContext时才表明是一个未处理的子协程异常,
//否则可能是supervisorScope未配置CoroutineExceptionHandler导致的异常传递,此时根协程会进行执行coroutineContext:$coroutineContext ref.get():${ref.get()}", )
throwable.printStackTrace()
if (coroutineContext == ref.get()) {
handleException(throwable, errorHandler, onComplete, onFinally)
}
}) {
ref.set(this.coroutineContext)
try {
val result = block()
onComplete?.invoke(Result.success(result))
onFinally?.invoke(Result.success(result))
} catch (e: Exception) {
if (e is CancellationException) {
onCancel?.invoke()
onFinally?.invoke(Result.failure(e))
} else {
throw e
}
}
}
}

private fun <T> handleException(
e: Throwable,
errorHandler: ((exception: Throwable) -> Unit)?,
onComplete: ((Result<T>) -> Unit)? = null,
onFinally:((Result<T>) -> Unit)? = null
) {
e.printStackTrace()
//异常处理
try {
errorHandler?.invoke(e)
} catch (e: Exception) {
e.printStackTrace()
}
try {
onComplete?.invoke(Result.failure(e))
} catch (e: Exception) {
e.printStackTrace()
}
try {
onFinally?.invoke(Result.failure(e))
} catch (e: Exception) {
e.printStackTrace()
}
}

代码测试正常异常处理,但实际上这段代码存在逻辑隐患,读者朋友可以仔细思考下,问题出在哪里了;
下面给出异常case,在Android activity oncreate方法中执行以下方法:

1
2
3
4
5
6
7
8
9
private fun testLaunchWithCatch() {
lifecycleScope.launchWithCatch {
while (isActive) {
// do loop operate
println(">>>>I'm do loop")
delay(1000)
}
}
}

我们发现即使activity退出了,协程依然在工作,>>>>I'm do loop会始终输出;
问题原因就在于,SupervisorJob()的加入导致了原先的父子结构发生了变化;他的继承关系是SupervisorJob–〉JobImpl–〉JobSupport–〉Job
回忆下文章开头协程获取设置父Job的场景,代码在类AbstractCoroutine的初始化函数中,

1
2
3
init {
if (initParentJob) initParentJob(parentContext[Job])
}

使用launchWithCatch导致新生成的协程不再是lifecycleScope的子协程,而是SupervisorJob的子协程,因此当activity页面lifecycleScope cancel时,刚刚发起的协程无法正常关闭;

好了,问题已经知道了,提供解决办法就是让SupervisorJob变成lifecycleScope的子协程即可,代码如下

1
2
3
4
5
fun <T> CoroutineScope.launchWithCatch(
...
val parentJob = context[Job] ?: coroutineContext[Job]
return this.launch(context + SupervisorJob(parentJob) + CoroutineExceptionHandler {
...