}

}

val launchB = launch {

repeat(3) {

delay(100)

log(“launchB - $it”)

}

}

}

[main] launchA - 0

[main] launchB - 0

[main] launchA - 1

[main] launchB - 1

[main] launchA - 2

[main] launchB - 2

2、Job

Job 是协程的句柄。使用 launch 或 async 创建的每个协程都会返回一个 Job 实例,该实例唯一标识协程并管理其生命周期。Job 是一个接口类型,这里列举 Job 几个比较有用的属性和函数

//当 Job 处于活动状态时为 true

//如果 Job 未被取消或没有失败,则均处于 active 状态

public val isActive: Boolean

//当 Job 正常结束或者由于异常结束,均返回 true

public val isCompleted: Boolean

//当 Job 被主动取消或者由于异常结束,均返回 true

public val isCancelled: Boolean

//启动 Job

//如果此调用的确启动了 Job,则返回 true

//如果 Job 调用前就已处于 started 或者是 completed 状态,则返回 false

public fun start(): Boolean

//用于取消 Job,可同时通过传入 Exception 来标明取消原因

public fun cancel(cause: CancellationException? = null)

//阻塞等待直到此 Job 结束运行

public suspend fun join()

//当 Job 结束运行时(不管由于什么原因)回调此方法,可用于接收可能存在的运行异常

public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle

Job 具有以下几种状态值,每种状态对应的属性值各不相同

StateisActiveisCompletedisCancelledNew (optional initial state)falsefalsefalseActive (default initial state)truefalsefalseCompleting (transient state)truefalsefalseCancelling (transient state)falsefalsetrueCancelled (final state)falsetruetrueCompleted (final state)falsetruefalse

fun main() {

//将协程设置为延迟启动

val job = GlobalScope.launch(start = CoroutineStart.LAZY) {

for (i in 0…100) {

//每循环一次均延迟一百毫秒

delay(100)

}

}

job.invokeOnCompletion {

log(“invokeOnCompletion:$it”)

}

log(“1. job.isActive:${job.isActive}”)

log(“1. job.isCancelled:${job.isCancelled}”)

log(“1. job.isCompleted:${job.isCompleted}”)

job.start()

log(“2. job.isActive:${job.isActive}”)

log(“2. job.isCancelled:${job.isCancelled}”)

log(“2. job.isCompleted:${job.isCompleted}”)

//休眠四百毫秒后再主动取消协程

Thread.sleep(400)

job.cancel(CancellationException(“test”))

//休眠四百毫秒防止JVM过快停止导致 invokeOnCompletion 来不及回调

Thread.sleep(400)

log(“3. job.isActive:${job.isActive}”)

log(“3. job.isCancelled:${job.isCancelled}”)

log(“3. job.isCompleted:${job.isCompleted}”)

}

[main] 1. job.isActive:false

[main] 1. job.isCancelled:false

[main] 1. job.isCompleted:false

[main] 2. job.isActive:true

[main] 2. job.isCancelled:false

[main] 2. job.isCompleted:false

[DefaultDispatcher-worker-2] invokeOnCompletion:java.util.concurrent.CancellationException: test

[main] 3. job.isActive:false

[main] 3. job.isCancelled:true

[main] 3. job.isCompleted:true

3、async

看下 async 函数的方法签名。async 也是一个作用于 CoroutineScope 的扩展函数,和 launch 的区别主要就在于:async 可以返回协程的执行结果,而 launch 不行

public fun CoroutineScope.async(

context: CoroutineContext = EmptyCoroutineContext,

start: CoroutineStart = CoroutineStart.DEFAULT,

block: suspend CoroutineScope.() -> T

): Deferred

通过await()方法可以拿到 async 协程的执行结果,可以看到两个协程的总耗时是远少于七秒的,总耗时基本等于耗时最长的协程

fun main() {

val time = measureTimeMillis {

runBlocking {

val asyncA = async {

delay(3000)

1

}

val asyncB = async {

delay(4000)

2

}

log(asyncA.await() + asyncB.await())

}

}

log(time)

}

[main] 3

[main] 4070

由于 launch 和 async 仅能够在 CouroutineScope 中使用,所以任何创建的协程都会被该 scope 追踪。Kotlin 禁止创建不能够被追踪的协程,从而避免协程泄漏

4、async 的错误用法

修改下上述代码,可以发现两个协程的总耗时就会变为七秒左右

fun main() {

val time = measureTimeMillis {

runBlocking {

val asyncA = async(start = CoroutineStart.LAZY) {

delay(3000)

1

}

val asyncB = async(start = CoroutineStart.LAZY) {

delay(4000)

2

}

log(asyncA.await() + asyncB.await())

}

}

log(time)

}

[main] 3

[main] 7077

会造成这不同区别是因为 CoroutineStart.LAZY不会主动启动协程,而是直到调用async.await()或者async.satrt()后才会启动(即懒加载模式),所以asyncA.await() + asyncB.await()会导致两个协程其实是在顺序执行。而默认值 CoroutineStart.DEFAULT 参数会使得协程在声明的同时就被启动了(实际上还需要等待被调度执行,但可以看做是立即就执行了),所以即使 async.await()会阻塞当前线程直到协程返回结果值,但两个协程其实都是处于运行状态,所以总耗时就是四秒左右

此时可以通过先调用start()再调用await()来实现第一个例子的效果

asyncA.start()

asyncB.start()

log(asyncA.await() + asyncB.await())

5、async 并行分解

由 suspend 函数启动的所有协程都必须在该函数返回结果时停止,因此你可能需要保证这些协程在返回结果之前完成。借助 Kotlin 中的结构化并发机制,你可以定义用于启动一个或多个协程的 coroutineScope。然后,你可以使用 await()(针对单个协程)或 awaitAll()(针对多个协程)保证这些协程在从函数返回结果之前完成

例如,假设我们定义一个用于异步获取两个文档的 coroutineScope。通过对每个延迟引用调用 await(),我们可以保证这两项 async 操作在返回值之前完成:

suspend fun fetchTwoDocs() =

coroutineScope {

val deferredOne = async { fetchDoc(1) }

val deferredTwo = async { fetchDoc(2) }

deferredOne.await()

deferredTwo.await()

}

你还可以对集合使用 awaitAll(),如以下示例所示:

suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)

coroutineScope {

val deferreds = listOf( // fetch two docs at the same time

async { fetchDoc(1) }, // async returns a result for the first doc

async { fetchDoc(2) } // async returns a result for the second doc

)

deferreds.awaitAll() // use awaitAll to wait for both network requests

}

虽然 fetchTwoDocs() 使用 async 启动新协程,但该函数使用 awaitAll() 等待启动的协程完成后才会返回结果。不过请注意,即使我们没有调用 awaitAll(),coroutineScope 构建器也会等到所有新协程都完成后才恢复名为 fetchTwoDocs 的协程。此外,coroutineScope 会捕获协程抛出的所有异常,并将其传送回调用方

6、Deferred

async 函数的返回值是一个 Deferred 对象。Deferred 是一个接口类型,继承于 Job 接口,所以 Job 包含的属性和方法 Deferred 都有,其主要就是在 Job 的基础上扩展了 await()方法

七、CoroutineContext

CoroutineContext 使用以下元素集定义协程的行为:

Job:控制协程的生命周期

CoroutineDispatcher:将工作分派到适当的线程

CoroutineName:协程的名称,可用于调试

CoroutineExceptionHandler:处理未捕获的异常

1、Job

协程中的 Job 是其上下文 CoroutineContext 中的一部分,可以通过 coroutineContext[Job] 表达式从上下文中获取到

以下两个 log 语句虽然是运行在不同的协程上,但是其指向的 Job 其实是同个对象

fun main() = runBlocking {

val job = launch {

log(“My job is ${coroutineContext[Job]}”)

}

log(“My job is $job”)

}

[main @coroutine#1] My job is “coroutine#2”:StandaloneCoroutine{Active}@75a1cd57

[main @coroutine#2] My job is “coroutine#2”:StandaloneCoroutine{Active}@75a1cd57

实际上 CoroutineScope 的 isActive 这个扩展属性只是 coroutineContext[Job]?.isActive == true 的一种简便写法

public val CoroutineScope.isActive: Boolean

get() = coroutineContext[Job]?.isActive ?: true

2、CoroutineDispatcher

CoroutineContext 包含一个 CoroutineDispatcher(协程调度器)用于指定执行协程的目标载体,即运行于哪个线程。CoroutineDispatcher 可以将协程的执行操作限制在特定线程上,也可以将其分派到线程池中,或者让它无限制地运行。所有的协程构造器(如 launch 和 async)都接受一个可选参数,即 CoroutineContext ,该参数可用于显式指定要创建的协程和其它上下文元素所要使用的 CoroutineDispatcher

要在主线程之外运行代码,可以让 Kotlin 协程在 Default 或 IO 调度程序上执行工作。在 Kotlin 中,所有协程都必须在 CoroutineDispatcher 中运行,即使它们在主线程上运行也是如此。协程可以自行暂停,而 CoroutineDispatcher 负责将其恢复

Kotlin 协程库提供了四个 Dispatcher 用于指定在何处运行协程,大部分情况下我们只会接触以下三个:

Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用 suspend 函数、运行 Android 界面框架操作,以及更新 LiveData 对象

Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作

Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON

fun main() = runBlocking {

launch {

log(“main runBlocking”)

}

launch(Dispatchers.Default) {

log(“Default”)

}

launch(Dispatchers.IO) {

log(“IO”)

}

launch(newSingleThreadContext(“MyOwnThread”)) {

log(“newSingleThreadContext”)

}

}

[DefaultDispatcher-worker-1 @coroutine#3] Default

[DefaultDispatcher-worker-2 @coroutine#4] IO

[MyOwnThread @coroutine#5] newSingleThreadContext

[main @coroutine#2] main runBlocking

当 launch {…} 在不带参数的情况下使用时,它从外部的协程作用域继承上下文和调度器,即和 runBlocking 保持一致。而在 GlobalScope 中启动协程时默认使用的调度器是 Dispatchers.default,并使用共享的后台线程池,因此 launch(Dispatchers.default){…} 与 GlobalScope.launch{…} 是使用相同的调度器。newSingleThreadContext 用于为协程专门创建一个新的线程来运行,专用线程是一种成本非常昂贵的资源,在实际的应用程序中必须在不再需要时释放掉,或者存储在顶级变量中以便在整个应用程序中进行重用

**

- 3、withContext

**

对于以下代码,get方法内使用withContext(Dispatchers.IO) 创建了一个指定在 IO 线程池中运行的代码块,该区间内的任何代码都始终通过 IO 线程来执行。由于 withContext 方法本身就是一个挂起函数,因此 get 方法也必须定义为挂起函数

suspend fun fetchDocs() { // Dispatchers.Main

val result = get(“developer.android.com”) // Dispatchers.Main

show(result) // Dispatchers.Main

}

suspend fun get(url: String) = // Dispatchers.Main

withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)

/* perform network IO here */ // Dispatchers.IO (main-safety block)

} // Dispatchers.Main

}

借助协程,你可以细粒度地来调度线程。由于withContext()支持让你在不引入回调的情况下控制任何代码的执行线程池,因此你可以将其应用于非常小的函数,例如从数据库中读取数据或执行网络请求。一种不错的做法是使用 withContext() 来确保每个函数都是主线程安全的,这意味着,你可以从主线程调用每个函数。这样,调用方就从不需要考虑应该使用哪个线程来执行函数了

在前面的示例中,fetchDocs() 方法在主线程上执行;不过,它可以安全地调用 get方法,这样会在后台执行网络请求。由于协程支持 suspend 和 resume,因此 withContext 块完成后,主线程上的协程会立即根据 get 结果恢复

与基于回调的等效实现相比,withContext() 不会增加额外的开销。此外在某些情况下,还可以优化 withContext() 调用,使其超越基于回调的等效实现。例如,如果某个函数对一个网络进行十次调用,你可以使用外部 withContext() 让 Kotlin 只切换一次线程。这样,即使网络库多次使用 withContext(),它也会留在同一调度程序上,并避免切换线程。此外,Kotlin 还优化了 Dispatchers.Default 与 Dispatchers.IO 之间的切换,以尽可能避免线程切换

利用一个使用线程池的调度程序(例如 Dispatchers.IO 或 Dispatchers.Default)不能保证代码块一直在同一线程上从上到下执行。在某些情况下,Kotlin 协程在 suspend 和 resume 后可能会将执行工作移交给另一个线程。这意味着,对于整个 withContext() 块,线程局部变量可能并不指向同一个值

4、CoroutineName

CoroutineName 用于为协程指定一个名字,方便调试和定位问题

fun main() = runBlocking(CoroutineName(“RunBlocking”)) {

log(“start”)

launch(CoroutineName(“MainCoroutine”)) {

launch(CoroutineName(“Coroutine#A”)) {

delay(400)

log(“launch A”)

}

launch(CoroutineName(“Coroutine#B”)) {

delay(300)

log(“launch B”)

}

}

}

[main @RunBlocking#1] start

[main @Coroutine#B#4] launch B

[main @Coroutine#A#3] launch A

5、CoroutineExceptionHandler

在下文的异常处理会讲到

6、组合上下文元素

有时我们需要为协程上下文定义多个元素,那就可以用 + 运算符。例如,我们可以同时为协程指定 Dispatcher 和 CoroutineName

fun main() = runBlocking {

launch(Dispatchers.Default + CoroutineName(“test”)) {

log(“Hello World”)

}

}

[DefaultDispatcher-worker-1 @test#2] Hello World

此外,由于 CoroutineContext 是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext。比如,(Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)

八、取消协程

如果用户退出某个启动了协程的 Activity/Fragment 的话,那么大部分情况下就应该取消所有协程

job.cancel()就用于取消协程,job.join()用于阻塞等待协程运行结束。因为 cancel() 函数调用后会马上返回而不是等待协程结束后再返回,所以此时协程不一定就是已经停止运行了。如果需要确保协程结束运行后再执行后续代码,就需要调用 join() 方法来阻塞等待。也可以通过调用 Job 的扩展函数 cancelAndJoin() 来完成相同操作,它结合了 cancel 和 join两个操作

un main() = runBlocking {

fval job = launch {

repeat(1000) { i ->

log(“job: I’m sleeping $i …”)

delay(500L)

}

}

delay(1300L)

log(“main: I’m tired of waiting!”)

job.cancel()

job.join()

log(“main: Now I can quit.”)

}

[main] job: I’m sleeping 0 …

[main] job: I’m sleeping 1 …

[main] job: I’m sleeping 2 …

[main] main: I’m tired of waiting!

[main] main: Now I can quit.

1、协程可能无法取消

并不是所有协程都可以响应取消操作,协程的取消操作是需要协作(cooperative)完成的,协程必须协作才能取消。协程库中的所有挂起函数都是可取消的,它们在运行时会检查协程是否被取消了,并在取消时抛出 CancellationException 从而结束整个任务。但如果协程正在执行计算任务并且未检查是否已处于取消状态的话,就无法取消协程

所以即使以下代码主动取消了协程,协程也只会在完成既定循环后才结束运行,因为协程没有在每次循环前先进行检查,导致任务不受取消操作的影响

fun main() = runBlocking {

val startTime = System.currentTimeMillis()

val job = launch(Dispatchers.Default) {

var nextPrintTime = startTime

var i = 0

while (i < 5) {

if (System.currentTimeMillis() >= nextPrintTime) {

log(“job: I’m sleeping ${i++} …”)

nextPrintTime += 500L

}

}

}

delay(1300L)

log(“main: I’m tired of waiting!”)

job.cancelAndJoin()

log(“main: Now I can quit.”)

}

[DefaultDispatcher-worker-1] job: I’m sleeping 0 …

[DefaultDispatcher-worker-1] job: I’m sleeping 1 …

[DefaultDispatcher-worker-1] job: I’m sleeping 2 …

[main] main: I’m tired of waiting!

[DefaultDispatcher-worker-1] job: I’m sleeping 3 …

[DefaultDispatcher-worker-1] job: I’m sleeping 4 …

[main] main: Now I can quit.

为了实现取消协程的目的,就需要为上述代码加上判断协程是否还处于可运行状态的逻辑,当不可运行时就主动退出协程。isActive 是 CoroutineScope 的扩展属性,就用于判断协程是否还处于可运行状态

fun main() = runBlocking {

val startTime = System.currentTimeMillis()

val job = launch(Dispatchers.Default) {

var nextPrintTime = startTime

var i = 0

while (i < 5) {

if (isActive) {

if (System.currentTimeMillis() >= nextPrintTime) {

log(“job: I’m sleeping ${i++} …”)

nextPrintTime += 500L

}

} else {

return@launch

}

}

}

delay(1300L)

log(“main: I’m tired of waiting!”)

job.cancelAndJoin()

log(“main: Now I can quit.”)

}

取消协程这个操作类似于在 Java 中调用Thread.interrupt()方法来向线程发起中断请求,这两个操作都不会强制停止协程和线程,外部只是相当于发起一个停止运行的请求,需要依靠协程和线程响应请求后主动停止运行。Kotlin 和 Java 之所以均没有提供一个可以直接强制停止协程或线程的方法,是因为这个操作可能会带来各种意想不到的情况。在停止协程和线程的时候,它们可能还持有着某些排他性资源(例如:锁,数据库链接),如果强制性地停止,它们持有的锁就会一直无法得到释放,导致其他协程和线程一直无法得到目标资源,最终可能导致线程死锁。所以Thread.stop()方法目前也是处于废弃状态,Java 官方并没有提供可靠的停止线程的方法

2、用 finally 释放资源

可取消的挂起函数在取消时会抛出 CancellationException,可以依靠try {…} finally {…} 或者 Kotlin 的 use 函数在取消协程后释放持有的资源

fun main() = runBlocking {

val job = launch {

try {

repeat(1000) { i ->

log(“job: I’m sleeping $i …”)

delay(500L)

}

} catch (e: Throwable) {

log(e.message)

} finally {

log(“job: I’m running finally”)

}

}

delay(1300L)

log(“main: I’m tired of waiting!”)

job.cancelAndJoin()

log(“main: Now I can quit.”)

}

[main] job: I’m sleeping 0 …

[main] job: I’m sleeping 1 …

[main] job: I’m sleeping 2 …

[main] main: I’m tired of waiting!

[main] StandaloneCoroutine was cancelled

[main] job: I’m running finally

[main] main: Now I can quit.

3、NonCancellable

如果在上一个例子中的 finally 块中再调用挂起函数的话,将会导致抛出 CancellationException,因为此时协程已经被取消了。通常我们并不会遇到这种情况,因为常见的资源释放操作都是非阻塞的,且不涉及任何挂起函数。但在极少数情况下我们需要在取消的协程中再调用挂起函数,此时可以使用 withContext 函数和 NonCancellable上下文将相应的代码包装在 withContext(NonCancellable) {…} 代码块中,NonCancellable 就用于创建一个无法取消的协程作用域

fun main() = runBlocking {

log(“start”)

val launchA = launch {

try {

repeat(5) {

delay(50)

log(“launchA-$it”)

}

} finally {

delay(50)

log(“launchA isCompleted”)

}

}

val launchB = launch {

try {

repeat(5) {

delay(50)

log(“launchB-$it”)

}

} finally {

withContext(NonCancellable) {

delay(50)

log(“launchB isCompleted”)

}

}

}

//延时一百毫秒,保证两个协程都已经被启动了

delay(200)

launchA.cancel()

launchB.cancel()

log(“end”)

}

[main] start

[main] launchA-0

[main] launchB-0

[main] launchA-1

[main] launchB-1

[main] launchA-2

[main] launchB-2

[main] end

[main] launchB isCompleted

4、父协程和子协程

当一个协程在另外一个协程的协程作用域中启动时,它将通过 CoroutineScope.coroutineContext 继承其上下文,新启动的协程就被称为子协程,子协程的 Job 将成为父协程 Job 的子 Job。父协程总是会等待其所有子协程都完成后才结束自身,所以父协程不必显式跟踪它启动的所有子协程,也不必使用 Job.join 在末尾等待子协程完成

所以虽然 parentJob 启动的三个子协程的延时时间各不相同,但它们最终都会打印出日志

fun main() = runBlocking {

val parentJob = launch {

repeat(3) { i ->

launch {

delay((i + 1) * 200L)

log(“Coroutine $i is done”)

}

}

log(“request: I’m done and I don’t explicitly join my children that are still active”)

}

}

[main @coroutine#2] request: I’m done and I don’t explicitly join my children that are still active

[main @coroutine#3] Coroutine 0 is done

[main @coroutine#4] Coroutine 1 is done

[main @coroutine#5] Coroutine 2 is done

5、传播取消操作

一般情况下,协程的取消操作会通过协程的层次结构来进行传播。如果取消父协程或者父协程抛出异常,那么子协程都会被取消。而如果子协程被取消,则不会影响同级协程和父协程,但如果子协程抛出异常则也会导致同级协程和父协程被取消

对于以下代码,子协程 jon1 被取消并不影响子协程 jon2 和父协程继续运行,但父协程被取消后子协程都会被递归取消

fun main() = runBlocking {

val request = launch {

val job1 = launch {

repeat(10) {

delay(300)

log(“job1: $it”)

if (it == 2) {

log(“job1 canceled”)

cancel()

}

}

}

val job2 = launch {

repeat(10) {

delay(300)

log(“job2: $it”)

}

}

}

delay(1600)

log(“parent job canceled”)

request.cancel()

delay(1000)

}

[main @coroutine#3] job1: 0

[main @coroutine#4] job2: 0

[main @coroutine#3] job1: 1

[main @coroutine#4] job2: 1

[main @coroutine#3] job1: 2

[main @coroutine#3] job1 canceled

[main @coroutine#4] job2: 2

[main @coroutine#4] job2: 3

[main @coroutine#4] job2: 4

[main @coroutine#1] parent job canceled

6、withTimeout

withTimeout 函数用于指定协程的运行超时时间,如果超时则会抛出 TimeoutCancellationException,从而令协程结束运行

fun main() = runBlocking {

log(“start”)

val result = withTimeout(300) {

repeat(5) {

delay(100)

}

200

}

log(result)

log(“end”)

}

[main] start

Exception in thread “main” kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms

at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:186)

at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:156)

at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:497)

at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)

at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:69)

at java.lang.Thread.run(Thread.java:748)

withTimeout方法抛出的 TimeoutCancellationException 是 CancellationException 的子类,之前我们并未在输出日志上看到关于 CancellationException 这类异常的堆栈信息,这是因为对于一个已取消的协程来说,CancellationException 被认为是触发协程结束的正常原因。但对于withTimeout方法来说,抛出异常是其上报超时情况的一种手段,所以该异常不会被协程内部消化掉

如果不希望因为异常导致协程结束,可以改用withTimeoutOrNull方法,如果超时就会返回 null

九、异常处理

当一个协程由于异常而运行失败时,它会传播这个异常并传递给它的父协程。接下来,父协程会进行下面几步操作:

取消它自己的子级

取消它自己

将异常传播并传递给它的父级

异常会到达层级的根部,而且当前 CoroutineScope 所启动的所有协程都会被取消,但协程并非都是一发现异常就执行以上流程,launch 和 async 在处理异常方面有着很大的差异

launch 将异常视为未捕获异常,类似于 Java 的 Thread.uncaughtExceptionHandler,当发现异常时就会马上抛出。async 期望最终是通过调用 await 来获取结果 (或者异常),所以默认情况下它不会抛出异常。这意味着如果使用 async 启动新的协程,它会静默地将异常丢弃,直到调用 async.await() 才会得到目标值或者抛出存在的异常

例如,以下代码中 launchA 抛出的异常会先连锁导致 launchB 也被取消(抛出 JobCancellationException),然后再导致父协程 BlockingCoroutine 也被取消

fun main() = runBlocking {

val launchA = launch {

delay(1000)

1 / 0

}

val launchB = launch {

try {

delay(1300)

log(“launchB”)

} catch (e: CancellationException) {

e.printStackTrace()

}

}

launchA.join()

launchB.join()

}

kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=BlockingCoroutine{Cancelling}@5eb5c224

Caused by: java.lang.ArithmeticException: / by zero

at coroutines.CoroutinesMainKt$main

1

1

1launchA$1.invokeSuspend(CoroutinesMain.kt:11)

···

Exception in thread “main” java.lang.ArithmeticException: / by zero

at coroutines.CoroutinesMainKt$main

1

1

1launchA$1.invokeSuspend(CoroutinesMain.kt:11)

···

1、CoroutineExceptionHandler

如果不想将所有的异常信息都打印到控制台上,那么可以使用 CoroutineExceptionHandler 作为协程的上下文元素之一,在这里进行自定义日志记录或异常处理,它类似于对线程使用 Thread.uncaughtExceptionHandler。但是,CoroutineExceptionHandler 只会在预计不会由用户处理的异常上调用,因此在 async 中使用它没有任何效果,当 async 内部发生了异常且没有捕获时,那么调用 async.await() 依然会导致应用崩溃

以下代码只会捕获到 launch 抛出的异常

fun main() = runBlocking {

val handler = CoroutineExceptionHandler { _, exception ->

log(“Caught $exception”)

}

val job = GlobalScope.launch(handler) {

throw AssertionError()

}

val deferred = GlobalScope.async(handler) {

throw ArithmeticException()

}

joinAll(job, deferred)

}

[DefaultDispatcher-worker-2] Caught java.lang.AssertionError

2、SupervisorJob

由于异常导致的取消在协程中是一种双向关系,会在整个协程层次结构中传播,但如果我们需要的是单向取消该怎么实现呢?

例如,假设在 Activity 中启动了多个协程,如果单个协程所代表的子任务失败了,此时并不一定需要连锁终止整个 Activity 内部的所有其它协程任务,即此时希望子协程的异常不会传播给同级协程和父协程。而当 Activity 退出后,父协程的异常(即 CancellationException)又应该连锁传播给所有子协程,终止所有子协程

可以使用 SupervisorJob 来实现上述效果,它类似于常规的 Job,唯一的区别就是取消操作只会向下传播,一个子协程的运行失败不会影响到其他子协程

例如,以下示例中 firstChild 抛出的异常不会导致 secondChild 被取消,但当 supervisor 被取消时 secondChild 也被同时取消了

fun main() = runBlocking {

val supervisor = SupervisorJob()

with(CoroutineScope(coroutineContext + supervisor)) {

val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {

log(“First child is failing”)

throw AssertionError(“First child is cancelled”)

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

文末

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

sor 被取消时 secondChild 也被同时取消了

fun main() = runBlocking {

val supervisor = SupervisorJob()

with(CoroutineScope(coroutineContext + supervisor)) {

val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {

log(“First child is failing”)

throw AssertionError(“First child is cancelled”)

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-kEqsXMOM-1711933009616)]

[外链图片转存中…(img-i14vWA24-1711933009617)]

[外链图片转存中…(img-3PD0Q5mT-1711933009617)]

[外链图片转存中…(img-5lZCRSvS-1711933009617)]

[外链图片转存中…(img-deL12ekO-1711933009618)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

文末

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家

[外链图片转存中…(img-3p8HC86I-1711933009618)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

相关文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: