首先
先说总结,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被移除掉了,本质原因在文章开头说过了,这个集合中的每个元素都有一个唯一的Key,item3,item5虽然是2个对象,但是他们具有相同的Key,所以在执行plus操作时,原先的item3会被移除掉,结果就是item4,item5。
最后
使用一个实际工作的例子来结束这篇文章。如果之前没有用过异常处理器的小伙伴,建议阅读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()
}
}
}