相关文章:

Android进阶宝典 — Kotlin协程剖析(创立、撤销、超时)

1 组合挂起函数 – async

组合挂起函数,其实便是将多个挂起函数组合,然后输出一个终究的成果,例如有下面这个场景:

有两个接口,需求从服务端拉取成果,然后将两个成果进行整合

咱们经过定义两个挂起函数,模仿从服务端获取成果,为什么是挂起函数,是由于必定需求扔到协程中履行,再者便是在进行网络恳求时,可以被撤销,由于只需挂起函数才能被撤销。

 * 第一个接口,1s后回来一个成果
 */
suspend fun httpOne():Int{
    delay(1000)
    return 25
}
/**
 * 第二个接口,1.5s后回来一个成果
 */
suspend fun httpTwo():Int{
    delay(1500)
    return 20
}

那么当履行两个挂起函数时,下面这种方法其实是串行履行的,当httpOne履行完结之后,再履行httpTwo,为什么呢?其实这有点相似于经过coroutineScope构建了一个协程效果域,当履行httpOne时,runBlocking会被挂起,等到履行完毕之后再康复履行httpTwo。

fun testHttp() = runBlocking {
    val time = measureTimeMillis {
        val httpOne = httpOne()
        val httpTwo = httpTwo()
        Log.e("TAG","httpOne $httpOne httpTwo $httpTwo")
    }
    Log.e("TAG","cost time $time")
}

假如前后有依靠关系,例如httpTwo需求httpOne成果,那么这样做无可厚非该花的时间仍是要继续花,假如两者没有前后依靠关系,只需求取到各自的履行成果再做处理,明显上面的方法是比较耗时,这时咱们可以考虑开启子协程。

var httpOne = 0
var httpTwo = 0
fun testHttp() = runBlocking {
    launch {
        httpOne = httpOne()
    }
    launch {
        httpTwo = httpTwo()
    }
}

那么当runBlocking中的子协程悉数履行完毕之后,runBlocking就会退出,此刻依照耗时最长的方法httpTwo,此刻拿到悉数的成果只需求1.5s。

1.1 async完结协程并发

已然咱们目的便是为了拿到两个接口的回来值,其实上面的处理方法并不太优雅,标准的处理方法应该是经过async创立协程,此刻调用await就可以挂起等待成果回来, 终究拿到两个接口的回来值。

fun testHttp() = runBlocking {
    val time = measureTimeMillis {
        val one = async { httpOne() }
        val two = async { httpTwo() }
        Log.e("TAG", "one ${one.await()} two ${two.await()}")
    }
    Log.e("TAG", "cost time $time")
}
2023-06-22 17:05:09.917 31690-31690/com.lay.nowinandroid E/TAG: one 25 two 20
2023-06-22 17:05:09.917 31690-31690/com.lay.nowinandroid E/TAG: cost time 1516

经过成果咱们发现,这个时分完结了协程的并发,终究耗时为1.5s,咱们看下await函数,

public suspend fun await(): T

它也是一个挂起函数,当调用await时会挂起其时协程,但不会堵塞其时线程,因而运用async可以完结不堵塞线程的并发使命。

经过async创立协程之后,会马上履行协程效果域中的代码,假如在某些场景中,咱们可能不会马上恳求接口,在做一些计算之后,才会发动协程,那么这个时分可以经过”懒汉“的方法创立协程,将其发动形式设置为CoroutineStart.LAZY

fun testHttp() = runBlocking {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { httpOne() }
        val two = async(start = CoroutineStart.LAZY) { httpTwo() }
        Log.e("TAG", "one ${one.await()} two ${two.await()}")
    }
    Log.e("TAG", "cost time $time")
}

那么这种方法创立的协程,只需在调用await或许start的时分,才会发动协程履行效果域内的代码。可是是否调用start方法,关于协程履行的次序有着直接的影响。

只是履行await时,httpOne和httpTwo两个函数履行的次序为串行的,其实就跟没有async相同;所以假如想要完结结构化的并发,必须要在调用await之前,调用start。

fun testHttp() = runBlocking {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { httpOne() }
        val two = async(start = CoroutineStart.LAZY) { httpTwo() }
        val result = awaitAll(one,two)
        Log.e("TAG", "one ${result[0]} two ${result[1]}")
    }
    Log.e("TAG", "cost time $time")
}

当然假如咱们不想显示地调用start方法,可以直接调用awaitAll函数, 这儿可以传入一组Deferred目标,在其内部就会调用start和await,终究回来一组成果,与传入的Deferred目标次序共同。

1.2 async结构化并发反常

当咱们运用async创立异步使命时,看下面的代码,

suspend fun testHttp2(): Int = coroutineScope {
    val job1 = async { httpOne() }
    val job2 = async { httpTwo() }
    job1.await() + job2.await()
}

其实和之前写的相似,其实都是有问题的,是由于当某个async协程出现问题之后,协程效果域内的悉数协程都会被撤销,例如httpOne函数内部产生反常,那么httpTwo就不会正常履行了。

咱们来模仿一下这个场景,httpTwo在履行时抛出了IllegalArgumentException反常:

/**
 * 第一个接口,1s后回来一个成果
 */
suspend fun httpOne(): Int {
    try {
        delay(Long.MAX_VALUE)
        return 25
    } finally {
        Log.e("TAG", "httpOne was cancelled")
    }
}
/**
 * 第二个接口,1.5s后回来一个成果
 */
suspend fun httpTwo(): Int {
    delay(1500)
    throw IllegalArgumentException("模仿抛出反常")
}

那么此刻httpOne还没有回来成果,可是delay挂起函数会检测到协程被撤销从而停止使命。

MainScope().launch {
    try {
        val result = testHttp2()
    }catch (e:Exception){
        Log.e("TAG","testHttp2 error $e")
    }
}

当履行testHttp2函数时,咱们发现httpOne被撤销了。

2023-06-22 20:44:05.460 6123-6123/com.lay.nowinandroid E/TAG: httpOne was cancelled
2023-06-22 20:44:05.462 6123-6123/com.lay.nowinandroid E/TAG: testHttp2 error java.lang.IllegalArgumentException: 模仿抛出反常

所以当咱们选用async完结结构化并发,要注意这种情况的产生,那么如何确保协程之间不受搅扰呢?会在4.2末节中介绍,感兴趣的同伴可以往下翻,这儿咱们需求记住一点便是,当一个协程出现反常时,效果域内的其他协程也会被撤销。

1.3 launch和async的差异比较

关于launch和async的差异比较,咱们从下面几个方面看异同;

  • 回来值

经过源码咱们可以看出来,launch回来的是Job目标,而async回来的是Deferred目标,其中Deferred是承继自Job,包括Job一切的特点,因而假如想撤销协程时可以调用cancel;多出来的一个方法便是await,可以延迟回来成果。

  • 是否堵塞线程

launch和async都不会堵塞线程,当launch协程效果域内履行挂起函数时,协程会被挂起,当async履行await函数时,也会挂起协程。当协程被挂起时,可释放底层线程干其他的事。

  • 是否需求等待成果回来

launch不会堵塞到拿到成果之后才回来,而async假如不调用await和launch相同,只需调用await之后,才会挂起协程,一向堵塞到成果回来。

  • 反常传达

关于launch和async反常传达的不同,会在4.1节中介绍。

所以当需求履行一个无成果的耗时使命时,例如删除文件,可以运用launch;而假如履行一个需求成果回来的耗时使命,例如网络恳求,可以运用async。并且运用async可以代替传统的回调函数,避免出现回调地狱。

2 协程的上下文

前面咱们经过介绍协程的根本用法,想必同伴们关于协程已经有了必定的概念,接下来将会具体介绍协程的上下文,这部分就会触及到线程的调度,可以愈加深入理解协程的概念。

2.1 什么是协程上下文

在Android中,咱们关于上下文并不生疏,像Activity、Service等四大组件都存在上下文Context,并且在组件中都可以直接拿到上下文运用,那么Kotlin协程中的上下文CoroutineContext,与Android中的上下文有异曲同工之妙。

已然咱们能在Activity中拿到上下文,那么在协程中当然也可以拿到上下文,当咱们创立一个协程之后,回来的Job目标便是协程的上下文主元素,除此之外,协程的上下文还包括其他的元素,例如协程调度器Dispatchers。当咱们拿到上下文之后,还可以操作协程,例如履行撤销操作cancel,说明从上下文中可以拿到协程,相似于在Context中可以拿到Activity。

2.2 协程调度器

在构建协程的时分,协程构建器launch可接收一个context参数,也便是CoroutineContext协程上下文,其中可以声明其时协程的调度器。

MainScope().launch(context = Dispatchers.Main + Job()) {
    try {
        val result = testHttp2()
    }catch (e:Exception){
        Log.e("TAG","testHttp2 error $e")
    }
}

协程调度器可以确定其时协程在哪个线程上履行,例如主线程Main,子线程IO,或许默许Default,也可以分配在某个线程池中履行,也可以设置其不受限制地运行。

接下来经过代码看下协程调度器的效果:

lifecycleScope.launch {
    Log.e("TAG","no params work on ${Thread.currentThread().name}")
}
lifecycleScope.launch(Dispatchers.Main) {
    Log.e("TAG","Main work on ${Thread.currentThread().name}")
}
lifecycleScope.launch(Dispatchers.IO) {
    Log.e("TAG","IO work on ${Thread.currentThread().name}")
}
lifecycleScope.launch(Dispatchers.Default) {
    Log.e("TAG","Default work on ${Thread.currentThread().name}")
}
lifecycleScope.launch(Dispatchers.Unconfined) {
    Log.e("TAG","Unconfined work on ${Thread.currentThread().name}")
}

这儿运用了Activity或许Fragment中需求运用的协程效果域构建器lifecycleScope创立协程,终究拿到的成果咱们看:

2023-06-23 12:58:39.596 9111-9111/com.lay.nowinandroid E/TAG: Unconfined work on main
2023-06-23 12:58:39.596 9111-9147/com.lay.nowinandroid E/TAG: Default work on DefaultDispatcher-worker-3
2023-06-23 12:58:39.596 9111-9144/com.lay.nowinandroid E/TAG: IO work on DefaultDispatcher-worker-1
2023-06-23 12:58:39.699 9111-9111/com.lay.nowinandroid E/TAG: no params work on main
2023-06-23 12:58:39.701 9111-9111/com.lay.nowinandroid E/TAG: Main work on main

接下来咱们挨个剖析

  • 不传参数构建协程
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

经过源码咱们可以看到,当不传参数时,默许的协程上下文为EmptyCoroutineContext,此刻它会从发动它的CoroutineScope中承继上下文,也便是lifecycleScope的上下文

val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
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
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

经过源码咱们可以发现,其实lifecycleScope效果域的上下文是Dispatchers.Main,跟主线程绑定的,因而经过lifecycleScope发动的协程,其上下文也是在主线程中。

  • Dispatchers.Main + Dispatchers.IO

这两个就不需求过多赘述了,从命名来看便是协程运行在主线程或许子线程

  • Dispatchers.Default

Default选用的是体系的默许调度器,也是作业在子线程,归于同享的线程池,当运用GlobalScope创立协程时,运用的调度器也是Dispatchers.Default。

  • Dispatchers.Unconfined

从字面意思上来看,是散漫、自由、不受限制,因而它会作业在主线程中,可是只是维持到第一次挂起,康复之后具体作业在哪个线程,完全由挂起函数来决定

lifecycleScope.launch(Dispatchers.Unconfined) {
    Log.e("TAG","Unconfined work on ${Thread.currentThread().name}")
    delay(500)
    Log.e("TAG","After Unconfined work on ${Thread.currentThread().name}")
}
2023-06-23 13:39:38.058 16249-16249/com.lay.nowinandroid E/TAG: Unconfined work on main
2023-06-23 13:39:38.578 16249-16281/com.lay.nowinandroid E/TAG: After Unconfined work on kotlinx.coroutines.DefaultExecutor

看到上面这个比如就能看到,在delay之前是作业在主线程,可是调用delay挂起之后,就会作业在一个线程池中,所以一瞬间在主线程,一瞬间不在主线程,显得极为散漫,所以这种调度器一般适用于不占用CPU,或许不需求刷新UI的使命

除此之外,还可以经过newSingleThreadContext上下文,切当地创立一个新的线程,但这种方法并不引荐运用,触及到了新建线程势必会消耗体系资源。

2.3 协程上下文中的Job

其实关于Job咱们并不生疏,前面咱们在讲协程创立的时分,不管是launch仍是async,终究的回来值都是Job或许Job的子类,在任意协程效果域中,都可以经过上下文来获取对应的Job,所以除了协程调度器之外,Job也是上下文中的一部分元素。

val job = lifecycleScope.launch {
    val job = coroutineContext[Job]
    Log.e("TAG","no params work on ${Thread.currentThread().name} job $job")
}
Log.e("TAG","launch job $job")
2023-06-23 14:51:02.347 28175-28175/com.lay.nowinandroid E/TAG: launch job StandaloneCoroutine{Active}@69e0010
2023-06-23 14:47:00.149 27410-27410/com.lay.nowinandroid E/TAG: no params work on main job StandaloneCoroutine{Active}@69e0010

这儿咱们发现job目标为StandaloneCoroutine,并且其时协程是Active活泼的,假如看过launch的源码,应该可以看到默许创立的Job目标便是StandaloneCoroutine,并且与launch创立回来的job是共同的。

@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
public val CoroutineScope.isActive: Boolean
    get() = coroutineContext[Job]?.isActive ?: true

所以之前咱们在讲协程撤销的时分,在协程效果域中直接调用isActive扩展特点,其实拿到的便是Job的isActive特点。

2.3.1 Job的世袭机制

当咱们经过父协程效果域创立子协程时,经过前面的知识,咱们知道子协程会承继自父协程的上下文,那么此刻子协程的Job就会成为父协程Job的子Job,那么在撤销父协程的时分,会递归撤销子协程

首先咱们先看下关于父协程上下文的承继,咱们看下面的比如:

fun testCoroutineContext() = runBlocking {
    launch(Dispatchers.IO + CoroutineName("parent")) {
        Log.e("TAG", "testCoroutineContext: parent context $coroutineContext")
        launch {
            Log.e("TAG", "testCoroutineContext: child context $coroutineContext")
        }
    }
}
2023-06-23 18:15:03.357 31109-31143/com.lay.nowinandroid E/TAG: testCoroutineContext: parent context [CoroutineName(parent), StandaloneCoroutine{Active}@72b055a, Dispatchers.IO]
2023-06-23 18:15:03.358 31109-31143/com.lay.nowinandroid E/TAG: testCoroutineContext: child context [CoroutineName(parent), StandaloneCoroutine{Active}@68ee38b, Dispatchers.IO]

咱们看到,当创立一个新的协程之后,会创立新的Job目标,可是CoroutineName和Dispatcher都是承继自父协程,假如子协程想要覆盖父协程上下文,那么就需求手动声明。

当需求撤销父协程时,例如:

fun testJob() = runBlocking {
    val job = launch {
        Log.e("TAG", "parent current thread ${Thread.currentThread().name}")
        // JOB1
        launch {
            delay(100)
            Log.e("TAG", "child current thread ${Thread.currentThread().name}")
            delay(500)
            Log.e("TAG", "may be cancelled----")
        }
        // JOB2
        GlobalScope.launch {
            Log.e("TAG", "GlobalScope start")
            delay(1000)
            Log.e("TAG", "do it always")
        }
    }
    delay(200)
    job.cancel()
}

例如JOB1是经过父协程效果域创立的,那么JOB1协程的job就归于父协程Job的子Job;而JOB2则是经过GlobalScope创立一个协程,此协程与应用程序生命周期绑定,与父协程无关,因而当父协程撤销之后,只需JOB1被撤销,而JOB2正常履行。

2023-06-23 15:39:19.994 5109-5109/com.lay.nowinandroid E/TAG: parent current thread main
2023-06-23 15:39:20.001 5109-5143/com.lay.nowinandroid E/TAG: GlobalScope start
2023-06-23 15:39:20.118 5109-5141/com.lay.nowinandroid E/TAG: child current thread DefaultDispatcher-worker-1
2023-06-23 15:39:21.040 5109-5141/com.lay.nowinandroid E/TAG: do it always

所以咱们可以这么理解,一个父协程总是等待一切子协程JOB完结之后,才会完毕;一旦父协程完毕,那么内部悉数子协程(经过父协程效果域创立的)都会完毕

2.4 组合上下文

前面咱们说到,协程的上下文其实是一组元素,那么关于这一组元素例如:调度器、Job、协程名等,假如组合在一起,作为CoroutineContext传到launch参数中呢,是可以运用+运算符。

launch(Dispatchers.IO + Job() + CoroutineName("子协程Job1")) {
    delay(100)
    Log.e("TAG", "child current thread ${Thread.currentThread().name}")
    delay(500)
    Log.e("TAG", "may be cancelled----")
}

其时不是随意一个目标都可以扔到里面,咱们看下每个上下文元素的源码。

public interface Element : CoroutineContext {
    /**
     * A key of this coroutine context element.
     */
    public val key: Key<*>
    public override operator fun <E : Element> get(key: Key<E>): E? =
        @Suppress("UNCHECKED_CAST")
        if (this.key == key) this as E else null
    public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
        operation(initial, this)
    public override fun minusKey(key: Key<*>): CoroutineContext =
        if (this.key == key) EmptyCoroutineContext else this
}

一切上下文元素的基类,都是Element,它有一个抽象完结类AbstractCoroutineContextElement,像Dispatchers、CoroutineName都是完结了这个类,而Job则是完结了Element。

3 协程的发动形式

在第二末节中,咱们剖析了协程的上下文,再回头看看launch函数的第一个参数咱们就剖析完结了,接下来便是第二个参数CoroutineStart。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

CoroutineStart,从字面意思来看便是发动形式,这块在官方文档中其实没有讲到,但这却是一个比较重要的知识点,当咱们创立协程的时分,默许便是DEFAULT发动形式,除此之外,CoroutineStart还有以下几种发动形式:LAZY、ATOMIC、UNDISPATCHED,咱们逐个剖析。

3.1 CoroutineStart.DEFAULT发动形式

val job = scope.launch {
    Log.e("TAG","协程创立了---->预备履行代码了")
    delay(2000)
    Log.e("TAG","协程履行完结了!")
}
//撤销协程
Log.e("TAG","我要撤销协程了---->")
job.cancel()
2023-06-23 21:51:40.471 5781-5781/com.lay.nowinandroid E/TAG: 我要撤销协程了---->
end

这是咱们经常会运用的创立协程的方法,此刻发动形式为DEFAULT,当协程创立完结之后,就会马上进入调度状况,那么只需这个协程没有履行完结,在任意时间都可以被撤销,哪怕协程还没有创立完结。

3.2 CoroutineStart.ATOMIC发动形式

val job = scope.launch(start = CoroutineStart.ATOMIC) {
    Log.e("TAG","协程创立了---->预备履行代码了")
    delay(2000)
    Log.e("TAG","协程履行完结了!")
}
//撤销协程
Log.e("TAG","我要撤销协程了---->")
job.cancel()
2023-06-23 21:55:53.483 6431-6431/com.lay.nowinandroid E/TAG: 我要撤销协程了---->
2023-06-23 21:55:53.587 6431-6431/com.lay.nowinandroid E/TAG: 协程创立了---->预备履行代码了
end

这种发动形式与DEFAULT大致相同,相同也是在协程创立完结之后马上进行调度,可是在第一个挂起点之前,不会呼应撤销操作。

咱们看协程第一个挂起点为履行delay函数,所以在此之前即便咱们履行了cancel方法,可是delay之前的代码仍然可以正常履行,只需当协程被挂起之后,才会呼应撤销操作。

3.3 CoroutineStart.LAZY发动形式

val job = scope.launch(start = CoroutineStart.LAZY) {
    Log.e("TAG","协程创立了---->预备履行代码了")
    delay(2000)
    Log.e("TAG","协程履行完结了!")
}
//撤销协程
Log.e("TAG","我要撤销协程了---->")
job.cancel()
job.start()
2023-06-23 22:00:25.723 7129-7129/com.lay.nowinandroid E/TAG: 我要撤销协程了---->
end

这种发动形式,咱们在async这末节中介绍过,LAZY其实就相当于懒加载,当协程创立完结之后,不会马上调度,而是当调用launch的start或许async的await时,才会进入调度。 所以假如在此之前履行撤销操作,那么协程效果域内的代码便不会履行。

3.4 CoroutineStart.UNDISPATCHED发动形式

val job = scope.launch(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
    Log.e("TAG","协程创立了---->预备履行代码了 ${Thread.currentThread().name}")
    delay(2000)
    Log.e("TAG","协程履行完结了!")
}
//撤销协程
Log.e("TAG","我要撤销协程了---->")
job.start()
2023-06-23 22:04:59.349 7942-7942/com.lay.nowinandroid E/TAG: 协程创立了---->预备履行代码了 main
2023-06-23 22:04:59.357 7942-7942/com.lay.nowinandroid E/TAG: 我要撤销协程了---->
2023-06-23 22:05:01.400 7942-7979/com.lay.nowinandroid E/TAG: 协程履行完结了!
end

这种发动形式,一句话“爱谁谁”。尽管咱们在上下文中指定了运行在IO线程,可是发动形式设置为UNDISPATCHED之后,此刻Dispatcher将不再进行线程切换,承继了创立协程效果域的上下文,在main线程履行,所以这种发动形式一般很少用,风险比较大。

4 协程的反常处理

前面咱们在介绍协程撤销时,说到过当协程被撤销之后,会抛出CancellationException,可是由于协程的内部反常处理机制将其静默处理掉了,可是假如在协程中抛出其他的反常,是没有这种兜底战略的,所以咱们需求关注一下关于协程反常的处理。

4.1 launch和async反常处理

咱们知道,launch和async都会创立一个协程,那么假设现在悉数都在根协程中产生了反常,

  • 顶层协程产生反常
launch {
    delay(200)
    throw IllegalArgumentException("产生反常了~")
}

假设在父协程中抛出了IllegalArgumentException一场,此刻在控制台会打印反常信息,app产生了crash。

 Caused by: java.lang.IllegalArgumentException: 产生反常了~

像这种场景下,咱们想到在Java中处理反常的方法,咱们可以运用try-catch捕获反常,于是咱们选用了下面的这种方法去捕获反常。

 try {
    launch {
        delay(200)
        throw IllegalArgumentException("产生反常了~")
    }
 } catch (e: Exception) {
     Log.e("TAG", "捕获到了反常 $e")
 }

成果发现仍是溃散了,这是为什么呢?分明这儿已经加了catch成果仍是溃散了,所以这儿需求记住一个知识点,经过launch发动的顶层协程(父协程)是不会传达反常的

什么是传达反常?便是在协程中出现反常之后,是否会将反常抛给协程所在的线程中,假如不会,那么咱们加try-catch的意义就没有了,会直接导致应用溃散。

那么,假如是选用async创立协程产生反常时,咱们发现在调用await时,是可以捕获反常的。

val scope = MainScope()
val job = scope.async {
    delay(200)
    throw IllegalArgumentException("协程抛出反常~")
}
scope.launch {
    try {
        job.await()
    }catch (e:Exception){
        Log.e("TAG","await 捕获了反常")
    }
}

也便是说,当运用async创立顶层协程的时分,反常是传达的,并且是可以经过try-catch捕获到对应的反常。

  • 子协程产生反常

这儿咱们不再讨论launch创立子协程的反常,由于无法进行反常传达,只需子协程产生了反常,一切的协程都会被撤销,这儿咱们看下async创立子协程之后,是否还会进行反常传达。

val scope = MainScope()
scope.launch {
    //async创立一个子协程
    val job = async {
        delay(200)
        throw IllegalArgumentException("协程抛出反常~")
    }
    try {
        job.await()
    }catch (e:Exception){
        Log.e("TAG","await 捕获了反常")
    }
}
2023-06-23 20:50:32.096 27028-27028/com.lay.nowinandroid E/TAG: await 捕获了反常

这儿咱们在scope创立的协程效果域下经过async又创立一个子协程,此刻经过try-catch捕获反常,发现反常已经捕获到了,可是app产生了crash,并且即便是不履行await,仍然产生了反常

难道说反常传达不再起效果了吗?其实不管经过async创立顶层协程仍是子协程,反常都会传达,并且均可被try-catch捕获到,可是app产生了crash,这种情况下,就需求请出另一个上下文元素来协助处理。

4.2 CoroutineExceptionHandler大局反常捕获

前面咱们说到,经过launch创立的协程产生反常,或许经过async创立的子协程产生反常,都会导致app产生crash,此刻处理计划便是经过CoroutineExceptionHandler来处理。

val handler = CoroutineExceptionHandler { _, exp ->
    Log.e("TAG", "get exp $exp")
}
val scope = MainScope()
scope.launch(handler) {
    //async创立一个子协程
    val job = async {
        delay(200)
        throw IllegalArgumentException("协程抛出反常~")
    }
    job.await()
}
2023-06-23 20:59:58.806 28727-28727/com.lay.nowinandroid E/TAG: get exp java.lang.IllegalArgumentException: 协程抛出反常~

当经过MainScope创立一个顶层协程时,将CoroutineExceptionHandler作为上下文传递到launch中,此刻就可以捕获其时协程和子协程的悉数反常,并且确保app不产生crash。

假如熟悉java反常机制的同伴应该了解,Java中有一个Thread.UncautchExceptionHandler大局反常捕获,其实CoroutineExceptionHandler与其有些相似,可是Java中只能搜集反常信息,可是app仍是会crash。

val handler = CoroutineExceptionHandler { _, exp ->
    Log.e("TAG", "get exp $exp")
}
val scope = MainScope()
scope.launch(handler) {
    launch {
        delay(2000)
        Log.e("TAG","协程1履行完结")
    }
    launch {
        delay(500)
        throw IOException()
    }
}

关于launch协程不传达反常的问题,咱们经过CoroutineExceptionHandler也可以捕获到反常,可是这儿就会有一个问题,尽管app不会crash,可是由于一个协程产生了反常,导致了悉数的协程都被撤销了,有什么方法可以确保协程之间互不搅扰呢?

4.2.1 supervisorScope 和 SupervisorJob

要确保协程之间相互不搅扰,第一种计划便是经过supervisorScope构建一个协程效果域,在此效果域内协程之间履行不受搅扰,即便某个协程产生了反常,其他协程正常履行

val handler = CoroutineExceptionHandler { _, exp ->
    Log.e("TAG", "get exp $exp")
}
val scope = MainScope()
scope.launch(handler) {
    supervisorScope {
        launch {
            delay(2000)
            Log.e("TAG", "协程1履行完结")
        }
        launch {
            delay(500)
            throw IOException()
        }
    }
}
2023-06-23 21:10:48.073 30496-30496/com.lay.nowinandroid E/TAG: get exp java.io.IOException
2023-06-23 21:10:49.557 30496-30496/com.lay.nowinandroid E/TAG: 协程1履行完结

第二种方法便是经过指定SupervisorJob上下文,例如子协程2就指定了SupervisorJob上下文,此刻产生反常时,协程1并没有影响。

val handler = CoroutineExceptionHandler { _, exp ->
    Log.e("TAG", "get exp $exp")
}
val scope = MainScope()
scope.launch(handler) {
    launch {
        delay(2000)
        Log.e("TAG", "协程1履行完结")
    }
    launch(SupervisorJob()) {
        delay(500)
        throw IOException()
    }
}

其实经过指定SupervisorJob上下文,效果便是阻挠了反常向外传达,或许说向父协程传达,以此起到了协程间互不影响的效果。

4.2.2 反常聚合

前面咱们介绍了当反常产生时,一切子协程都会被撤销,所以一般情况下只会有一个反常交由顶层协程处理,假设在撤销协程时,又抛出一个反常,此刻会怎样处理呢?

val handler = CoroutineExceptionHandler { _, exp ->
    Log.e("TAG", "get exp $exp suppressed ${exp.suppressed.contentToString()}")
}
val scope = MainScope()
scope.launch(handler) {
    launch {
        try {
            delay(2000)
        }catch (e:Exception){
            //协程撤销时,再抛出反常
            throw IllegalArgumentException()
        }
        Log.e("TAG", "协程1履行完结")
    }
    launch {
        delay(500)
        throw IOException()
    }
}
2023-06-23 21:30:34.190 2391-2391/com.lay.nowinandroid E/TAG: get exp java.io.IOException suppressed [java.lang.IllegalArgumentException]

其实这儿关于多个反常处理是有一套规矩的:

⼀般规矩是“取第⼀个反常”,因而将处理第⼀个反常。在第⼀个反常之后发⽣的一切其他反常都作为被抑制的反常绑定⾄第⼀个反常。

所以在获取反常的suppressed特点时,会发现它会存在在一组反常,例如抛出的第二个反常IllegalArgumentException,便是依照反常聚合的规矩来完结的。