CoroutineContext的作用

1,014 阅读5分钟

首先

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

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

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左向链表

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

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,该类定义如下

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

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

  • CoroutineScope.launch创建newContext时,combined[ContinuationInterceptor]
@ExperimentalCoroutinesApi
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    val combined = foldCopies(coroutineContext, context, true)
    val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
    return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
        debug + Dispatchers.Default else debug
}
  • AbstractCoroutine初始化时获取父Job时,parentContext[Job]
init {
    if (initParentJob) initParentJob(parentContext[Job])
}

其次

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

fun studyContext4() {
    val my3CoroutineName = My3CoroutineName("item3")
    val my4CoroutineName = My4CoroutineName("item4")
    val my5CoroutineName = My3CoroutineName("item5")

    val newElement = (my3CoroutineName + my4CoroutineName) + my5CoroutineName

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

输出如下

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

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

最后

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

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

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方法中执行以下方法:

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的初始化函数中,

init {
    if (initParentJob) initParentJob(parentContext[Job])
}

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

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

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

最后的最后

按照正常的剧本,不出意外的话此处要出现意外了。

上面launchWithCatch例子在大多数情况下似乎工作良好,但是他有个潜在的问题。我们为了捕获异常而创建的子协程SupervisorJob并没有显示的关闭,而协程中的父子结构是结构化的;子协程没有结束,则父协程也没有结束。

有人会反对说,平时使用launch,或async函数创建子协程时也没有调用额外代码子协程就自动结束了,怎么到你这儿了就没结束呢?

我们要知道launch,或async函数都是框架提供的包装函数,他们在挂起block运行结束后会在内部调用resumeWith方法,从而结束当前协程;喜欢原理的朋友建议阅读Kotlin协程源码分析-2 调用挂起函数Kotlin协程源码分析-3 调用挂起函数

好了,我们给出一个示例来证明之前例子的在实际使用在的错误场景。

private suspend fun testUnComplete() {
    supervisorScope {
        println("start supervisorScope")
        launchWithCatch {
            println("start launchWithCatch")
            delay(1000)
            println("end launchWithCatch")
        }
        println("end supervisorScope")
    }
    println("after supervisorScope")
}

我们会发现testUnComplete方法不会正常结束,而是一直挂起,原因就是SupervisorJob没有关闭;以后大家在自己创建协程的时候,一定要注意;最后给出完整版代码

/**
 * 内部增加了对launch方法的异常捕获,防止程序crash;
 * @param onComplete 协程正常执行完毕后的回调,请注意协程cancel不会回调该方法
 * @param onCancel 协程取消时调用本代码
 * @param errorHandler 协程发生异常时回调,请注意协程取消不属于异常
 * @param onFinally 协程结束回调,不管是正常还是异常,或是取消
 */
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请注意
    val supervisorJob = SupervisorJob(context[Job] ?: coroutineContext[Job])
    return this.launch( context + supervisorJob + CoroutineExceptionHandler { coroutineContext, throwable ->
        //只有当coroutineContext是根coroutineContext时才表明是一个未处理的子协程异常,
        //否则可能是supervisorScope未配置CoroutineExceptionHandler导致的异常传递,此时根协程会进行执行
        throwable.printStackTrace()
        if (coroutineContext == ref.get()) {
            handleException(throwable, errorHandler, onComplete, onFinally)
        }
    }) {
        ref.set(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
            }
        } finally {
            //将手动创造的Job关闭,否则coroutineScope调用该方法,协程无法结束
            supervisorJob.complete()
        }
    }
}