一个协程的异常引发的思考
问题
之前代码中有同事这样写,然后发现CoroutineExceptionHandler无法捕获协程异常,于是直接在协程出错处加上了try catch 才可以,🤔
1 | lifecycleOwner.lifecycleScope.launchWhenCreated { |
而我们我们按照如下的实现就是OK的
1 | viewModelScope.launch(Dispatchers.Main + CoroutineExceptionHandler { _, throwable -> |
知识储备
如果大家对这部分比较熟悉那么可以直接跳动结构化并发开始看了
Android中常用内置协程作用域
上面看到的viewModelScope、lifecycleOwner.lifecycleScope都是内置的自定义作用域,自定义协程作用域可以针对性的避免内存泄漏
1 | val coroutineContext : CoroutineContext = Dispatchers.Main + Job()//协程上下文 |
之前还有一种GlobalScope,作用于整个应用的生命周期,并且无法被取消,在Android中使用的话会导致内存泄漏
1 |
|
ViewModelScope
viewModelScope 对结构化并发 的贡献在于将一项扩展属性加入到 ViewModel 类中,从而在 ViewModel 销毁时自动地取消子协程。
1 | val ViewModel.viewModelScope: CoroutineScope |
ViewModel 类有个 ConcurrentHashSet 属性来存储任何类型的对象。CoroutineScope 就存储在这里。如果我们看下代码,getTag(JOB_KEY) 方法试图从中取回作用域。如果取回值为空,它将创建一个新的 CoroutineScope 并将其加标签存储。
当 ViewModel 被清空时,它会运行 clear() 方法进而调用,在 clear() 方法中,ViewModel 会取消 viewModelScope 中的任务。
1 |
|
这个方法遍历所有对象并调用 closeWithRuntimeException,此方法检查对象是否属于 Closeable 类型,如果是就关闭它。为了使作用域被 ViewModel 关闭,它应当实现 Closeable 接口。这就是为什么 viewModelScope 的类型是 CloseableCoroutineScope,这一类型扩展了 CoroutineScope、重写了 coroutineContext 并且实现了 Closeable 接口。(SupervisorJob是什么后面会介绍)
1 | val viewModeScope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main) |
1 | internal final class CloseableCoroutineScope public constructor(context: kotlin.coroutines.CoroutineContext) : java.io.Closeable, kotlinx.coroutines.CoroutineScope { |
小结一下:
- CloseableCoroutineScope 从CoroutineScope派生出来,默认是主线程,Job是SupervisorJob
- 创建ViewModel的时候,会通过ViewModelStore以HashMap的形式把ViewModel保存起来;
- ViewModelStore中的clear()方法,是由Lifecycle在生命周期执行到onDestroy,触发ViewModelStore中也有个clear()方法,会循环调用ViewModel中的clear()方法,解决了内存泄漏问题
LifecycleScope
具有生命周期的协程。 它是LifecycleOwner生命周期所有者的扩展属性,与LifecycleOwner生命周期绑定,并会在LifecycleOwner生命周期destroyed的时候取(默认也是主线程)
lifecycleScope默认主线程,可以通过withContext来指定线程。
1 | public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch { |
共有三个对应生命周期的扩展函数:
- whenCreated
- whenStarted
- whenResumed继续看 lifecycle.coroutineScope
1
2val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope关键在于,通过LifecycleCoroutineScopeImpl创建了协程,默认主线程,随后又调用了newScope.register()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
if (mInternalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}
继续看下LifecycleCoroutineScopeImpl
1 | internal class LifecycleCoroutineScopeImpl( |
在register()方法中添加了LifecycleEventObserver接口的监听,LifecycleEventObserver会在onStateChanged方法中派发当前生命周期,关键来了,在onStateChanged回调中,判断当前生命周期是destroyed的时候,移除监听,并取消协程
1 | public abstract class LifecycleCoroutineScope internal constructor() : kotlinx.coroutines.CoroutineScope { |
小结下:
- LifecycleCoroutineScope也是从CoroutineScope派生出来 默认是主线程,Job是SupervisorJob
- 调用lifecycleScope,返回lifecycle.coroutineScope;
- 在coroutineScope中通过LifecycleCoroutineScopeImpl创建了协程,并调用了register()方法添加了对生命周期的监听,这个监听其实是为了在生命周期destroyed的时候取消协程;
小结
自定义协程
1 | val coroutineContext : CoroutineContext = Dispatchers.Main + Job()//协程上下文 |
AndroidX内置防止内存泄漏的协程Scope
1 | val viewModeScope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main) |
WithContext
withContext这个方法并不创建新的协程,只是切换到指定的线程中执行,并在闭包内的逻辑结束之后,自动把线程切换回去执行
1 | coroutineScope.launch(Dispatchers.Main) { // 在 UI 线程开始 |
通常是使用DisPatchers切换执行协程的线程,但是当我们使用
1 | withContext(Dispatchers.Main + CoroutineExceptionHandler { _, throwable -> |
CoroutineContext上下文
一组定义协程行为的元素,本体是一个数据结构,类似于Map,内部实现为单链表,由以下几个部分组成
- Job:执行的任务
- CoroutineDispatcher:协程调度器
- CoroutineName:协程的名称,主要用于调试
- CoroutineExceptionHandler:处理未被捕获的异常。
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
26public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext =
//空实现直接返回
if (context === EmptyCoroutineContext) this else
// 遍历context集合
context.fold(this) { acc, element -> //acc 当前上下文集合 element context集合的元素
val removed = acc.minusKey(element.key) //移除对应集合的元素
if (removed === EmptyCoroutineContext) element else {
val interceptor = removed[ContinuationInterceptor] //获取拦截器
if (interceptor == null) CombinedContext(removed, element) //生成最后的CombinedContext节点
else {
//拦截器永远位于 链表尾部
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
public fun minusKey(key: Key<*>): CoroutineContext
}自定义CoroutineContext
fold操作,通过+进行合并,会进行覆盖1
val coroutineContext : CoroutineContext = Dispatchers.Main + Job() + CoroutineName("name")//协程上下文
CoroutineContext的父子关系
每个协程都会有一个父对象,协程的父级CoroutineContext和父协程的CoroutineContext是不一致的。
父级上下文 = 默认值 + 继承的CoroutineContext+参数
- 默认值:一些元素包含的默认值,例如默认Dispatcher就是Dispatchers.Default
- 继承的CoroutineContext:父协程的CoroutineContenxt
- 参数:后续子协程配置的参数,如上文所示组成部分,新添加的参数会覆盖前面的对应配置。
结构化并发
在介绍协程的异常之前我们还需要搞清楚什么是结构化并发,这对我们理解协程异常的传递将十分有帮助
- 并发操作
比如在日常业务中,可能存在好几个需要同时处理的逻辑,比如同时请求两个网络接口,同时操作两个子任务等 - 结构化并发
每个操作其实都是处理一个单独的任务,这个任务可能包含子任务,每个任务都有自己的生命周期,子任务的生命周期会继承父任务的生命周期,如果父任务关闭子任务也会被取消
协程的异常
官网:协程异常处理
https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c
当一个协程由于一个异常而运行失败时,它会传播这个异常并传递给他的父级。

父协程通常会做以下3件事
- 取消其余子协程
- 取消自身
- 向它的父协程传递异常可以看出: 协程1异常. 协程2(兄弟协程)被取消. runBlocking(作用域)也被取消.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21runBlocking {
launch {
println("协程1-start") //2
delay(100)
throw Exception("Failed coroutine") //4
}
launch {
println("协程2-start") //3
delay(200)
println("协程2-end") //未打印
}
println("start") //1
delay(500)
println("end") //未打印
}
//结果如下
start
协程1-start
协程2-start
Exception in thread "main" java.lang.Exception: Failed coroutine ...
CoroutineExceptionHandler
除了trycatch外,协程内全局异常的捕获方式主要是使用CoroutineExceptionHandler
安卓-kotlin协程的异常处理机制分析
异常处理篇
1 | val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> |
如上可以捕获NPE。
CoroutineExceptionHandler类似Android中的全局异常处理,你可以理解为,类似 Thread.uncaughtExceptionHandler 一样。当异常在协程树中传递时,如果没有设置CoroutineExceptionHandler,那么异常将被继续传递直到抛出,但如果设置了CoroutineExceptionHandler,那么则可以在这里处理未捕获的异常,但是这里也是有条件限制的
- 异常是被自动抛出异常的协程所抛出的。(只能是 launch(),async()这种手动触发是不可以的)
- CoroutineExceptionHandler 位于 CoroutineScope 的 CoroutineContext 中(而非子协程),或 supervisorScope 的直接子协程中,或其他根协程中。
如下
1 | // 1. 初始化scope时 |
这样的设计是合理的,因为 CoroutineScope 的子协程不应该捕获异常,CoroutineScope 的设定就是子协程的异常交由父类处理,所以应该在 CoroutineScope 创建的根协程中捕获此异常。而 supervisorScope 的设定是子协程的异常自己处理,所以 supervisorScope 的子协程可以自己捕获异常。
SupervisorJob
在实际中这种结构化的异常处理,会让异常的处理有些暴力,大部分场景下,业务需求都是希望异常不影响正常的业务流程,因此提出了SupervisorJob的概念,它是Job的子类,
SupervisorJob的作用就是将协程中的异常「掐死」在异常协程内部,切断其向上传播的路径。
使用SupervisorJob后,子协程的异常退出不会影响到其他子协程,同时SupervisorJob也不会传播异常而是让异常发生的协程自己处理
SupervisorJob只有在supervisorScope或CoroutineScope(SupervisorJob())内执行才可以生效

从上面的ViewModelScope和LifecycleScope中我们可以看出都是使用了SupervisorJob,防止影响其他同级子协程
1 | val viewModeScope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main) |
这样我们在使用时候,只要在各个子协程中处理住异常就好,而不会影响其他子协程
1 | // Scope handling coroutines for a particular layer of my app |
因为协程具有结构化的特点,SupervisorJob 仅只能用于同一级别的子协程。如果我们在初始化 scope 时添加了 SupervisorJob ,那么整个scope对应的所有 根协程 都将默认携带 SupervisorJob ,否则就必须在 CoroutineContext 显示携带 SupervisorJob。
下面看2个例子感受一下
1 |
|
当协程A失败时,协程B依然可以打印
如果改成以下情况
1 | val scope = CoroutineScope(CoroutineExceptionHandler { _, _ -> }) |
则B协程无法正常打印,为什么呢,不是已经使用SupervisorJob()了吗,我们用一张图来看下
这里的核心问题是我们在scope.launch时传递了SupervisorJob,但是SupervisorJob不会传递到子协程,因为子协程在launch时会创建新的协程作用域,会使用默认新的Job替代我们传递的SurpervisorJob,因此会导致传递的SupervisorJob被覆盖。在这种情况下如果想子协程不影响父协程或者其他子协程就必须再显示添加SupervisorJob
1 | scope.launch { |
测试Case
在协程的作用域中,可以创建一个协程,同时一个协程可以继续创建协程,所以就形成了一个树形结构,借助这样的树形结构,协程可以很容易的控制结构化并发,父协程可以控制子协程的生命周期,而子协程可以从父协程继承协程上下文,但是如果发生了异常将会出现什么情况呢?
异常的传播首先跟作用域息息相关
- 顶级作用域 GlobalScope,异常不向外传播
- 并列协同 ,Job嵌套和coroutineScope创建,双向传播(本节的图一)
- 主从协同,通过supervisorScope创建,与内部子协程是主从(与外部协程协同关系),自上而下单向传播
从上面的分析也可以看出 ViewModelScope和LifecycleScope也属于此类scope(本节的图二)
举几个例子(均为直接launch 无任何异常捕获操作)
1、协程C1和协程C2 没有任何关系,都属于顶级作用域
1 | GlobalScope.launch { //协程C1 |
2、C2和C3是C1的子协程,C2和C3的异常会取消C1
1 | GlobalScope.launch { //协程C1 |
3、C2和C3是C1的子协程,由于使用supervisorScope C2和C3异常不会取消C1
1 | GlobalScope.launch { //协程C1 |
这里要注意下trycatch协程操作不当也是无效的,如下(如果我们使用trycatch 则需要放到协程体内部,捕获最初的异常本体,此处直接trycatch service.loadProjectTreeError())
1 | fun loadProjectTree() { |
子协程中未捕获的异常不会被重新抛出,而是在父子层次结构中向上传播,此时需要一个新的协程异常的处理
async 的协程需要try await操作才可,,此时结果和异常会包装在 返回值 Deferred.await()
try不住异常
1 | val scope = CoroutineScope(Job()) |
默认情况下,如果 异常没有被处理,而且顶级协程 CoroutineContext 中没有携带 CoroutineExceptionHandler ,则异常会传递给默认线程的 ExceptionHandler 。在 Android 中,如果没有设置 Thread.setDefaultUncaughtExceptionHandler , 这个异常将立即被抛出,从而导致引发App崩溃。
CoroutinexxHandler 不生效?
1 | val scope = CoroutineScope(Job()) |
根协程或者scope中没有设置 CoroutineExceptionHandler,异常会被直接抛出,所以这里肯定异常了,如何改正呢?scope 初始化时 或者 根协程里 加上 CoroutineExceptionHandler,或者直接 async 里面 try catch 都可以
或者使用下面的方式
1 | scope.launch { |
runCatching 是 kotlin 中对于 tryCatch 的一种包装,其会将结果使用 Result 类进行包装,从而让我们能更直观的处理结果,从而更加符合 kotlin 的语法习惯。
CoroutineScope
coroutineScope 的直接子协程不能捕捉到异常,因此需要在父协程中设置CoroutineExceptionHandler,当它的子协程发生异常时,即使不使用try catch,异常也会被捕获 (这个跟全文一开始提到实现1,是可以印证的)
比如下面例子
1 | val handler = CoroutineExceptionHandler { coroutineContext, throwable -> |
这个异常之所以能被捕获,就是因为 handler 是放在 scope.launch 中的,scope.launch 创建的协程属于根协程(父协程)。虽然我们抛出异常是在 scope 的子协程中,但子协程的异常会抛到父协程中处理,所以成功捕获了异常。
CoroutineScope 的子协程不应该捕获异常例子
1 | val handler = CoroutineExceptionHandler { coroutineContext, throwable -> |
handler 放在了 scope 子协程的子协程中,没有使用在正确的CoroutineContext上,这时异常会往父协程抛出,不会被自己捕获。
开篇的问题跟上面其实是如出一辙,lifecycleScope本质上也属于CoroutineScope,launchWhenCreated直接launch一个协程,作为父协程上下文并没有异常处理handler
1 | public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch { |
withContext只是切换执行线程并没有创建子协程,需要的是一个DisPatchers
1 | lifecycleOwner.lifecycleScope.launchWhenCreated { |
那么如何改呢,像之前一样在 HomePoplayerModel().loadConfig()中加trycatch当然也是可以的,或者可以使用launch的方式也是可以的,不过需要牺牲掉声明周期的特性了
1 | lifecycleScope.launch(handler) { //根协程 成功捕获异常 |
supervisorScope
但是作用域换成supervisorScope 那么情况又不一样了,因为这些 supervisorScope 内部的job为supervisorJob,当作用域中子协程异常时,异常不会主动层层向上传递,而是由子协程自行处理。子协程需要自己处理异常,这也就意味着可以为子协程增加CoroutineExceptionHandler
1 | fun main() { |
再看一个稍微复杂一点

当子协程异常时,因为我们使用了 supervisorScope ,所以异常此时不会主动传递给外部,而是由子类自行处理。
当我们在内部 launch 子协程时,其实也就是类似 scope.launch ,所以此时子协程A相也就是根协程,所以我们使用 CoroutineExceptionHandler 也可以正常拦截异常。但如果我们子协程不增加 CoroutineExceptionHandler ,则此时异常会被supervisorScope 抛出,然后被外部的 CoroutineExceptionHandler 拦截






