什么是协程 (Coroutine)
从广义上来讲,协程就代表了 互相协作的程序。
协程就像轻量级的线程,协程是依赖于线程,一个线程中可以创建N个协程,很重要的一点就是协程挂起时不会阻塞线程.
协程提供了一种避免阻塞线程并用更简单、更可控的操作替代线程阻塞的方法:协程挂起和恢复。
本质上Kotlin协程就是作为在Kotlin语言上进行异步编程的解决方案,处理异步代码的方法。
协程可以使用阻塞的方式写出非阻塞式的代码,解决并发中常见的回调地狱。消除了并发任务之间的协作的难度,协程可以让我们轻松地写出复杂的并发代码。一些本来不可能实现的并发任务变的可能,甚至简单,这些才是协程的优势所在。
协程 是 基于 线程 的 , 是 轻量级 线程 ;
在实际线程框架之上编写的用于管理并发的框架。最简单的定义是,可以在不阻塞线程的情况下挂起和恢复的代码片段。
协程的概念最核心的点就是一个函数或者一段程序能够被协程挂起,稍后再在挂起的位置恢复。
协程通过主动让出运行权来实现协作,程序自己处理挂起和恢复来实现程序执行流程的协作调度。因此它本质上就是在讨论程序控制流程的机制。
协程是一种可以挂起和恢复执行的轻量级线程。协程可以让我们以同步的方式编写异步代码,使得代码更加简洁易读。在Kotlin中,我们可以使用launch
或async
函数来创建并启动一个协程。
我们所有的代码都是跑在线程中的,而线程是跑在进程中的。协程没有直接和操作系统关联,但它不是空中楼阁,它也是跑在线程中的,可以是单线程(主线程或者子线程),也可以是多线程。如果在主线程进行网络请求,会抛出 NetworkOnMainThreadException
,对于在主线程上的协程 进行网络请求也会会抛出 NetworkOnMainThreadException
,这种场景使用协程还是要切换到子线程的。
协程的一个典型的使用场景就是线程控制,可以任意的切换不同的线程。
协程的作用
- 处理耗时任务 : 耗时任务 通常需要 阻塞主线程 , 线程量级太重 , 耗时任务 推荐在协程中执行 ;
- 保证主线程安全 : 从主线程中 安全地调用可能会挂起的函数 ;
异步任务 AsyncTask 也可以处理耗时操作 , 避免耗时任务阻塞线程 , 但是在 Android 11 中 , 官方规定 该 api 已过时 , 被标记为弃用状态 , 建议使用
协程
java.util.concurrent 包下的 Executor,ThreadPoolExecutor,FutureTask取代 AsyncTask
协程在Android上的主要使用场景:
- 1、线程切换,保证线程安全。
- 2、处理耗时任务(比如网络请求、解析
JSON
数据、从数据库中进行读写操作等)。
Kotlin为什么要使用协程
就像许多库一样,协程也是为了解决某一类问题而来,即主要用来简化异步编程,可以用同步的方式写出异步执行的代码,这一点比RxJava
的链式编程更加简便优雅。
一般异步编程时,最常见的就是使用Callback
,如果回调出现嵌套,代码结构层次会过多且混乱,出现大量模板式的回调处理,而协程不仅能消除大量的模板代码,而且能让异步执行的代码,像同步代码一样,顺序执行,同时又不阻塞当前线程。
我们使用 Retrofit
发起了一个异步请求,从服务端查询用户的信息,通过 CallBack
返回 response
。很明显我们需要处理很多的回调分支,如果业务多则更容易陷入「回调地狱」繁琐凌乱的代码中。
val call: Call<User> = userApi.getUserInfo("suming")
call.enqueue(object : Callback<User> {
//成功
override fun onResponse(call: Call<User>, response: Response<User>) {
val result = response.body()
result?.let { showUser(result) }
}
//失败
override fun onFailure(call: Call<User>, t: Throwable) {
showError(t.message)
}
})
但是如果我们使用协程,就可以解决以上的繁琐的「回调地狱」代码 。协程在写法上和普通的顺序代码类似,同步的方式去编写异步执行的代码。
GlobalScope.launch(Dispatchers.Main) {//开始协程:主线程
val result = userApi.getUserSuspend("suming")//网络请求(IO 线程)//挂起函数
tv_name.text = result?.name //更新 UI(主线程)
}
这就是kotlin最有名的【非阻塞式挂起】,使用同步的方式完成异步任务,而且很简洁,这是Kotlin协程的魅力所在。之所以用看起来同步的方式写异步代码,关键在于请求函数getUserSuspend()
是一个挂起函数,被suspend
关键字修饰。 在上面的协程的原理图解中,耗时阻塞的操作并没有减少,只是交给了其他线程(IO线程)。
userApi.getUserSuspend("suming")
真正执行的时候会切换到IO线程中执行,获取结果后最后恢复到主线程上,然后继续执行剩下的流程。
将业务流程原理拆分得更细致一点,在主线程中创建协程A
中执行整个业务流程,如果遇到异步调用任务则协程A
被挂起,切换到IO线程中创建子协程B
,去执行耗时的网络请求任务,获取结果后再恢复到主线程的协程A
上,然后继续执行剩下的流程。
协程的特点
轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
人性化:符合人类思维习惯 , 借助编辑器实现了 异步任务同步化 , 没有回调操作 ; 可以在执行一段程序后 挂起 , 之后在挂起的地方 恢复执行 ;
内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
Kotlin 协程分层架构
在 Kotlin 中 , 协程分为两层 :基础设施层 和 业务框架层
基础设施层
1.基础设施层的定义:
(1)基础设施层 : Kotlin 提供了 协程 标准库 Api , 为协程提供 概念 , 语义 支持 , 是 协程 实现的基础 ; Kotlin 协程的底层支持 ; 基础 Api ;
(2)基础设施层 : 基础设施层 的 协程基础 Api 定义在 kotlin.coroutines.*
包下 ;
import kotlin.coroutines.*
(3) 基础设施层: 类比的理解为是 Android 和 Java 的基础 Api ,
2.使用协程基础设施层标准库 Api 实现协程的案例:
第一步:协程 需要使用 协程体定义 , 协程体格式如下 :
suspend {
// 协程体内容
}
第二步:协程体定义完之后 , 调用协程体的 createCoroutine 函数 , 传入 Continuation 实例对象 , 一般传入一个 对象表达式 ( Java 中的匿名内部类 ) 作为参数 ;
对象表达式 object : Continuation<Int>
中 Continuation 后的 <Int> 泛型 表示的是协程体的返回值类型 ;
第三步:协程执行完毕后, 将协程执行结果返回 , 此时会回调 override fun resumeWith(result: Result<Int>)
函数 ;
private fun suspendCreate() {
// 创建协程
// 注意只是创建协程, 创建后还需要调用 resume 启动协程
val continuation = suspend{ // 协程体内容
// 协程体返回值为 int 类型 值 33
val result :User = mainViewModel.getUserSuspend()
result.id
}
// 调用协程体的 createCoroutine 函数 , 传入 Continuation 实例对象 , 一般传入一个 对象表达式 object : Continuation<Int>
//Continuation 后的 <Int> 泛型 表示的是协程体的返回值类型Int ;
.createCoroutine(object:Continuation<Int>{
// 协程上下文设置为 空的协程上下文 EmptyCoroutineContext
override val context: CoroutineContext= EmptyCoroutineContext
// 协程执行完毕后, 将协程执行结果返回
// 该函数是一个回调函数
override fun resumeWith(result: Result<Int>) {
Log.e(TAG, "协程体返回值为= $result") //协程体返回值为= Success(33)
}
})
//上面只是创建协程 , 如果要执行协程 , 还需要调用协程的 resume 方法 ;
continuation.resume(Unit)
}
业务框架层
1.业务框架层的定义:
(1)业务框架层 : Kotlin 协程的 上层框架 , 使用方便 ; 在之前博客中使用的 GlobalScope 类 , launch 函数 , delay 挂起函数 等都属于 业务框架层 , 都是 Kotlin 协程 的上层实现 ; 在 基础 Api 的基础上 进行了一层封装 , 形成了方便开发者使用的框架 ;
(2)业务框架层 : 如果调用 常见的协程 Api , 调用的是 业务框架层 的 Api , 如 GlobalScope , launch , Dispatchers 等 , 都定义在 kotlinx.coroutines.*
包下 ;
import kotlinx.coroutines.*
(3)业务框架层 :类比的理解为是 对 基础 Api 进行了一层封装的框架 , 如 RxJava , Retrofit , Glide 等 , 目的是为了方便开发 ;
协程的依赖库
在 project
的 gradle
添加 Kotlin
编译插件:
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
}
在app的 build.gradle
文件中添加依赖:
dependencies {
//协程标准库
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
//协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
//协程Android支持库,提供安卓UI调度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
}
线程与协程的区别与关系
⒈线程与协程的区别:
❶内存占用不同:
线程内存占用: 每个线程在创建时分配一定数量的栈内存(默认大约1MB)。如果系统启动大量线程,则会消耗大量内存,可能导致系统资源枯竭。
协程内存占用: 协程是运行在现有线程中的,它们不需要单独的栈内存,而是共享调用栈。这样使协程仅有少量内存开销,通常每个协程只占用几个KB。这使得同一线程可以管理和运行很多大量协程,不受传统线程数量限制。
❷任务切换不同:
线程切换: 线程切换由操作系统管理,涉及到用户态和内核态之间的切换,代价较高,需要保存和恢复CPU寄存器、程序计数器、内存栈等。
协程切换: 协程切换在用户态完成,不涉及内核态切换,只是切换函数的上下文,代价相对低很多。
❸内存模型不同:
线程内存模型:在JVM中,每个线程都有自己独立的线程栈,每个栈帧包含局部变量、操作数栈和相关信息。当线程被挂起时,所有这些信息必须保存并在重新调度时恢复。
协程内存模型:协程的栈帧通常是堆上的对象,当协程挂起时,不需要切换线程,只是函数调用的上下文发生变化,把协程状态保存到堆中。这种模型使得内存利用更加高效和灵活。协程在挂起时,会将当前的堆栈帧转换为对象并存储在堆中。这个对象包含了所有当前帧的局部变量、挂起点以及其他必要信息。恢复时,这个对象重新转换为堆栈帧并继续执行。
❹执行机制不同:
线程的执行机制:线程由操作系统调度,执行时获得CPU时间片,可能被抢占。这一过程完全由操作系统管理,且每次线程切换都会导致上下文切换,导致显著的内存开销。
协程的执行机制:协程仅占用一个线程的部分时间,是由协程库(例如 kotlinx.coroutines
)管理。一个线程可以执行多个协程,只要它们异步工作并时常挂起和恢复。这大大减少了切换开销,改善性能。
/*
启动了10万协程,但内存占用远远小于10万个线程,且切换效率高。
delay 使得协程挂起,但不阻塞线程,使得同一线程可以继续处理其他协程。
协程的上下文切换只需要保存和恢复堆上的对象状态,代价低。
由于协程不阻塞线程,上面的例子中,日志几乎是同时打印的
*/
fun main() = runBlocking {
repeat(100_000) {
launch {
delay(1000L)
println("Coroutine $it is done")
}
}
}
⒉线程与协程的关系:
协程Coroutine不能脱离线程而运行,但可以在不同的线程之间切换,而且一个线程上可以一个或多个不同的协程。
协程的构建(创建/构造器)CoroutineBuilder
负责创建和启动新协程的函数。例如,launch{}、async{}、runBlocking{}。
协程构建器launch{}、async{} 就是 CoroutineScope 协程作用域的两个扩展函数 ;
⒈launch创建的协程返回类型:Job
launch
函数是一种协程构建器,它用于创建并启动一个新的协程;launch
协程
不会阻塞
当前程序的执行流程,但是我们无法获取launch协程的执行结果
。它有点像是生活中的射箭。
通过 launch 启动一个协程以后,并没有让协程为我们返回一个执行结果,这其实就是典型的 Fire-and-forget
的应用场景。
launch 一个协程任务,就像猎人射箭一样:
- 箭一旦射出去了,
目标
就无法再被改变;协程一旦被 launch,那么它当中执行的任务
也不会被中途改变- 箭如果命中了猎物,
猎物
也不会自动送到我们手上来;launch 的协程任务一旦完成了,即使有了结果
,也没办法直接返回给调用方
launch 之所以无法将结果返回给调用方,是因为这个函数的返回值是一个 Job
,它代表的是协程的句柄
(Handle),它并不能为我们返回协程的执行结果。
创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。它返回的是一个该协程任务的引用,即Job
对象。这是最常用的用于启动协程的方式。
launch
函数返回一个Job
对象,我们可以使用这个对象来管理协程的生命周期; 但是该协程任务没有返回值,拿不到执行结果(注意:与async:Deferred<T>的区别
)
Job 对象的作用:
监测协程的生命周期状态;
操控协程;
launch 的函数声明分析
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
}
❶首先是 CoroutineScope.launch()
,代表了 launch 其实是一个扩展函数
,它的扩展接收者
类型是 CoroutineScope。前面我们使用的 GlobalScope
,就是官方提供的一个 CoroutineScope 对象。
val job = GlobalScope.launch(
context=CoroutineName("子协程job"),
start= CoroutineStart.LAZY,
block={ doSomething()}
)
job.cancel()
❷第一个参数 CoroutineContext
,代表了协程的上下文,它的默认值是 EmptyCoroutineContext
。也可以传入官方提供的 Dispatchers
来指定协程运行的线程池
。协程上下文是协程中非常关键的元素,
❸第二个参数 CoroutineStart
,代表了协程的启动模式,它的默认值是 CoroutineStart.DEFAULT
。这个枚举类的枚举值有:DEFAULT、LAZY、ATOMIC、UNDISPATCHED。最常用的是 DEFAULT (立即执行) 和 LAZY (懒加载执行)。
❹最后一个参数 block
,代表一个函数类型
() -> Unit 代表无参数、无返回的函数
suspend () -> Unit 代表无参数、无返回的挂起函数
suspend X.() -> Unit 代表这个函数是 X 类的成员方法或是扩展方法
案例1:
private fun launch_test1() {
GlobalScope.launch { // ① 启动一个协程。生产环境不建议使用 GlobalScope
Log.e(TAG, "协程开始执行_(${Thread.currentThread().name})") // ② 协程开始执行
delay(100L) // ③ 非阻塞延迟
Log.e(TAG, "协程执行完毕_(${Thread.currentThread().name})") // ④ 协程执行完毕
}
Log.e(TAG,"after launch_(${Thread.currentThread().name})") // ⑤
//Thread.sleep(120L) // ⑥ 让当前线程休眠,目的是不让主线程这么快退出
Log.e(TAG, "process exit_(${Thread.currentThread().name})") // ⑦
}
/** after launch_(main)
* process exit_(main)
* 通过打印日志可知: 通过 launch 创建的子协程还没来得及开始执行,整个程序执行就已经结束。
* 可以看到,launch 创建的子协程的执行任务, 并不会阻塞当前线程的执行。
* */
案例2:
private fun launch_test2() {
GlobalScope.launch { // ① 启动一个协程。生产环境不建议使用 GlobalScope
Log.e(TAG, "协程开始执行_(${Thread.currentThread().name})") // ② 协程开始执行
delay(100L) // ③ 非阻塞延迟
Log.e(TAG, "协程执行完毕_(${Thread.currentThread().name})") // ④ 协程执行完毕
}
Log.e(TAG,"after launch_(${Thread.currentThread().name})") // ⑤
Thread.sleep(120L) // ⑥ 让当前线程休眠,目的是不让主线程这么快退出
Log.e(TAG, "process exit_(${Thread.currentThread().name})") // ⑦
}
/**
* after launch_(main)
*协程开始执行_(DefaultDispatcher-worker-2)
*协程执行完毕_(DefaultDispatcher-worker-2)
* process exit_(main)
*
* 通过打印日志可知: Thread.sleep() 的作用就是让当前线程休眠,目的是不让主线程这么快退出,这样launch 创建的子协程就可以在主线程结束前,执行自己的任务逻辑
*
* */
⒉async创建的协程返回类型:Deferred<T>
使用 async 启动协程以后, async
协程
既不会阻塞
当前的执行流程,还可以直接获取
async协程的执行结果
。它有点像是生活中的钓鱼。
与launch
函数不同,async
函数返回一个Deferred
对象,这个对象表示一个可以延期获取结果的异步计算。 该协程任务(最后一行)会返回一个返回值T , 可以使用 .await()
函数可以获取协程的返回值 T;async 函数是 CoroutineScope 协程作用域 类的扩展函数 ;
创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。并返回Deffer
对象,可通过调用Deffer.await()
方法等待该子协程执行完成并获取结果。常用于并发执行-同步等待和获取返回值的情况。
/**
第一个参数 context: CoroutineContext = EmptyCoroutineContext 是协程的上下文对象 ;
第二个参数 start: CoroutineStart = CoroutineStart.DEFAULT 是协程的启动模式 ;
第三个参数 block: suspend CoroutineScope.() -> T 是协程作用域代码块 , 其中是协程任务代码 ;
*/
/**
* 创建协程并将其未来结果作为[Deferred]的实现返回。
* 当产生的延迟为[cancelled][Job.cancel]时,正在运行的协程将被取消。
* 得到的协程与其他语言中的类似原语相比有一个关键的区别
* 和框架:它取消父作业(或外部作用域)在执行*结构化并发*范式失败。
* 要改变这种行为,可以使用监督父级([SupervisorJob]或[supervisor orscope])。
*
* 协程上下文从[CoroutineScope]继承,附加的上下文元素可以用[context]参数指定。
* 如果上下文没有任何dispatcher,也没有任何其他[ContinuationInterceptor],则[Dispatchers.]默认使用“Default”。
* 父作业也从[CoroutineScope]继承,但它也可以被覆盖
* 使用相应的[上下文]元素。
*
* 默认情况下,协程是立即调度执行的。
* 其他选项可以通过' start '参数指定。参见[coroutinstart]了解详细信息。
* 可选参数[start]可以设置为[coroutinstart]。启动协同程序。在这种情况下,
* 结果[Deferred]是在_new_状态下创建的。它可以显式地以[start][Job.start]开始
* 函数,并将在第一次调用[join][Job时隐式启动。加入],[等待][递延。await]或[awaitAll]。
*
* @param block 协同程序代码。
*/
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
案例1:
launch 构建器 直接在 协程作用域 中实现协程任务 , 没有协程任务的返回值 ;
async{} 协程体中返回 String , 则调用 Deferred#await() 函数得到的是一个 String 类型对象 ;
如果在 async{} 协程体中返回 Int , 则调用 Deferred#await() 函数得到的是一个 Int 值 ;
private fun runBlockingTest2() {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
runBlocking(block={
// 在 runBlocking 代码块中 , 可以 直接调用 CoroutineScope 的扩展方法 , 如 launch , async 函数 ;
val launchJob1:Job = this@runBlocking.launch{
// 调用该挂起函数延迟 100 ms
delay(500)
Log.e(TAG,"launchJob1 执行")
"方明飞1" //这个值不会返回
}
Log.e(TAG,launchJob1.toString() ) //todo 协程没有返回结果值 StandaloneCoroutine{Active}@2546022
val launchJob2:Job =this@runBlocking.launch{
delay(400)
Log.e(TAG,"launchJob2 执行")
"方明飞2"
}
Log.e(TAG,launchJob2.toString() ) //todo 协程没有返回结果值 StandaloneCoroutine{Active}@f837ab3
val asyncJob3 : Deferred<Any> = this@runBlocking.async{
// 调用该挂起函数延迟 100 ms
delay(300)
Log.e(TAG,"asyncJob3 执行")
"方明飞3"
}
// 获取 asyncJob3 协程返回值
val asyncJob3_result = asyncJob3.await()
Log.e(TAG, "输出 asyncJob3 协程返回值 : ${asyncJob3_result}") //todo 输出 asyncJob3 协程返回值 : 方明飞3
})
//todo (默认根据delay延迟时间)输出各子协程内容的顺序 : asyncJob3 执行 launchJob2 执行 launchJob1 执行
}
案例2:
即使不调用 await() 方法,async 中的代码也会执行,因为 In async 在 await() 执行前就已经输出了
async 启动协程以后,不会阻塞当前程序的执行流程,因为 After async1 在 In async 的前面就已经输出了
async 的返回值是一个 Deferred 对象,通过其 await() 方法可以拿到协程的执行结果
class MainActivity2 : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
runBlocking {
val deferred: Deferred<String> = GlobalScope.async(block={
Log.e(TAG," 输出接受者对象this=${this}")
//todo 即使不调用 await() 方法,async 中的代码也会执行,因为 In async 在 await() 执行前就已经输出了
Log.e(TAG,"In async - ${Thread.currentThread().name}")
delay(100L) // 模拟耗时操作
return@async "bqt" // 返回执行结果
})
//todo async 启动协程以后,不会阻塞当前程序的执行流程,因为 After async1 在 In async 的前面就已经输出了
Log.e(TAG,"After async1 - ${Thread.currentThread().name}") // 注意他的执行顺序
Thread.sleep(110L) // 模拟耗时操作
Log.e(TAG,"After async2 - ${Thread.currentThread().name}") // 注意他的执行顺序
//todo async 的返回值是一个 Deferred 对象,通过其 await() 方法可以拿到协程的执行结果
val result = deferred.await() // 获取执行结果
Log.e(TAG,"Result is: $result")
/**
* After async1 - main
* 输出接受者对象this=DeferredCoroutine{Active}@a7315c5
* In async - DefaultDispatcher-worker-1
* After async2 - main
* Result is: bqt
* */
}
}
}
案例3:与挂起函数结合,再通过 async
并发来执行,是可以大大提升代码运行效率的。
下面代码里定义了三个挂起函数(各自做异步任务
),假设它们之间的运行结果互不相干
,且各自都会耗时 1000 毫秒,
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
runBlocking {
val resultsList :MutableList<String> = mutableListOf<String>()
Log.e(TAG,"输出集合resultsList:$resultsList")
val time = kotlin.system.measureTimeMillis(block={ // 计算总耗时
resultsList.add(mainViewModel.getResult1())
resultsList.add(mainViewModel.getResult2())
resultsList.add(mainViewModel.getResult3())
})
Log.e(TAG,"让我们看看总耗时Time: $time")
Log.e(TAG,"输出集合resultsList:$resultsList")
}
/** 输出集合resultsList:[]
* 让我们看看总耗时Time: 3001
* 输出集合resultsList:[Result1, Result2, Result3]
* */
}
}
class MainViewModel:ViewModel() {
// 挂起函数
suspend fun getResult1():String{
// 异步任务 耗时1s
delay(1000L).also{
return "Result1"
}
}
// 挂起函数
suspend fun getResult2():String{
// 异步任务 耗时1s
delay(1000L).also{
return "Result2"
}
}
// 挂起函数
suspend fun getResult3():String{
// 异步任务 耗时1s
delay(1000L).also{
return "Result3"
}
}
}
上面代码整个过程大约需要消耗 3000 毫秒,也就是这几个函数耗时的总和
。很明显这个不符合我们的业务逻辑执行效率低,请问该如何优化上面的代码?
对于这样的情况,我们其实完全可以使用 async
来优化:与挂起函数结合,再通过 async
并发来执行,通过一下案例,我们发现以下代码耗时仅仅才1000毫秒,大大提升代码运行效率的。
注意:请不要小看这个场景,在实际工作中,如果你仔细去分析
嵌套的异步代码
,你会发现,很多异步任务之间都是没有互相依赖
的,如果直接使用传统方式操作,即耗时还效率低下.所以,这样的代码结合挂起函数后,再通过async
并发来执行,是可以大大提升代码运行效率的。
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
runBlocking {
val resultsList :MutableList<String> = mutableListOf<String>()
Log.e(TAG,"输出集合resultsList:$resultsList")
val time = kotlin.system.measureTimeMillis(block={ // 计算总耗时
//这3个子协程同时创建 同时各自并列同时做各自的业务逻辑操作:异步任务
//创建一个子协程deferred1
val deferred1: Deferred<String> = async{mainViewModel.getResult1()}
//创建一个子协程deferred2
val deferred2: Deferred<String> = async { mainViewModel.getResult2() }
//创建一个子协程deferred3
val deferred3: Deferred<String> = async { mainViewModel.getResult3() }
resultsList.add(deferred1.await())
resultsList.add(deferred2.await())
resultsList.add(deferred3.await())
})
Log.e(TAG,"让我们看看总耗时Time: $time")
Log.e(TAG,"输出集合resultsList:$resultsList")
}
/** 输出集合resultsList:[]
* 让我们看看总耗时Time: 1036
* 输出集合resultsList:[Result1, Result2, Result3]
*/
}
}
class MainViewModel:ViewModel() {
// 挂起函数
suspend fun getResult1():String{
// 异步任务 耗时1s
delay(1000L).also{
return "Result1"
}
}
// 挂起函数
suspend fun getResult2():String{
// 异步任务 耗时1s
delay(1000L).also{
return "Result2"
}
}
// 挂起函数
suspend fun getResult3():String{
// 异步任务 耗时1s
delay(1000L).also{
return "Result3"
}
}
}
Job类介绍
Job
是协程的句柄。如果把门和门把手比作协程和Job
之间的关系,那么协程就是这扇门,Job
就是门把手。意思就是可以通过Job
实现对协程的控制和管理。
当我们用 launch 和 async 创建一个协程以后,同时也会创建一个对应的 Job 对象。另外,Job 也是我们理解协程生命周期、结构化并发的关键知识点。通过 Job 暴露的 API,我们还可以让不同的协程之间互相配合,从而实现更加复杂的功能。
public interface Job : CoroutineContext.Element { ... } // Job 接口
Job类 的常用 API
通过 Job 的 API 可以发现,Job(遥控器) 和协程(空调)的关系,有点像遥控器和空调的关系 :
❶遥控器可以
监测
空调的运行状态,Job 可以监测
协程的运行状态❷遥控器可以
操控
空调的运行状态,Job 可以简单操控
协程的运行状态❸所以,从某种程度来讲,遥控器是空调对外暴露的一个
句柄
,同样,Job 是协程的句柄, Job 可以
监测操控
协程的生命周期运行状态
public interface Job : CoroutineContext.Element {
// ------------ 状态查询 ------------
public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean
public fun getCancellationException(): CancellationException
// ------------ 操控状态 ------------
public fun start(): Boolean
public fun cancel(cause: CancellationException? = null)
public fun cancel(): Unit = cancel(null)
public fun cancel(cause: Throwable? = null): Boolean
// ------------ 等待状态 ------------
public suspend fun join()
public val onJoin: SelectClause0
// ------------ 完成状态回调API ------------
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
public fun invokeOnCompletion(
onCancelling: Boolean = false,
invokeImmediately: Boolean = true,
handler: CompletionHandler): DisposableHandle
// ...
}
Job#join() 挂起函数:
Job#join() 函数:launch 协程执行顺序控制
如果需要通过 launch 协程构建器 启动多个并行子协程 , 后面的子协程需要等待前面的子协程执行完毕 , 在启动靠后的协程 , 实现方案如下 :
调用 Job#join() 函数 , 可以挂起父协程 , 等待前面的launch中子协程体内的任务执行完毕 , 再执行后面的子协程任务 ;
Job#join()是个挂起函数 , 不会阻塞主线程 ;
join()的作用是:挂起当前的程序执行流程,待 job 中的协程任务执行完毕后,再恢复当前的程序执行流程
/**
* 挂起协程,直到此作业完成。此调用正常恢复(没有异常)
* 当作业因任何原因完成且调用协程的[job]仍为[active][isActive]时。
* 这个函数也[启动][Job。如果[Job]仍然处于_new_状态,则启动]相应的协程。
*
* 注意,只有当所有子任务都完成时,作业才算完成。
*
* 这个挂起函数是可取消的,并且**总是**检查是否取消了调用协程的Job。
* 如果调用协程的[Job]被取消或完成
* 函数被调用,或当它被挂起时,此函数
* 把[CancellationException]。
*
* 特别是,它意味着父协程在子协程上调用' join '时抛出
* [CancellationException]如果子进程失败,因为子进程的失败会默认取消父进程,
* 除非子进程是从[supervisor orscope]内部启动的。
*
* 此函数可用于带有[onJoin]子句的[select]调用。
* 使用[isCompleted]检查该作业是否已完成,无需等待。
*
* 有一个[cancelAndJoin]函数,它结合了[cancel]和' join '的调用。
*/
public suspend fun join()
案例:
private fun no_join_test(){
runBlocking {
val job = launch {
Log.e(TAG, "子协程job执行开始")
Log.e(TAG, "子协程job执行结束")
}
Log.e(TAG,"test1")
Log.e(TAG,"test2")
}
}
/** 执行顺序:
* test1
* test2
* 子协程job执行开始
* 子协程job执行结束
* */
private fun no_join_delay_test(){
runBlocking {
val job = launch {
delay(3000)
Log.e(TAG, "子协程job执行开始")
delay(5000)
Log.e(TAG, "子协程job执行结束")
}
Log.e(TAG,"test1")
Log.e(TAG,"test2")
}
}
/** 执行顺序:
* test1
* test2
* //挂起3000后执行
* 子协程job执行开始
* //挂起5000后执行
* 子协程job执行结束
* */
private fun no_join_delay_join_test(){
runBlocking {
val job = launch {
delay(3000)
Log.e(TAG, "子协程job执行开始")
delay(5000)
Log.e(TAG, "子协程job执行结束")
}
Log.e(TAG,"test1")
job.join() //管你子协程job里 是否挂起耗时操作 必须等子协程job执行完毕后 才会接着执行后面的逻辑
Log.e(TAG,"test2")
}
}
/** 执行顺序:
* test1
* //挂起3000后执行
* 子协程job执行开始
* //挂起5000后执行
* 子协程job执行结束
* test2
* */
Deferred类介绍
Deferred 是继承自 Job 的一个接口,它在 Job 的基础上扩展了一个 await()
方法:
public interface Deferred<out T> : Job { // 带泛型
public suspend fun await(): T // 挂起函数,有返回值
public val onAwait: SelectClause1<T>
@ExperimentalCoroutinesApi
public fun getCompleted(): T
@ExperimentalCoroutinesApi
public fun getCompletionExceptionOrNull(): Throwable?
}
Deferred#await() 函数:
await()
是一个挂起函数
,如果当前的 Deferred 任务还没执行完毕,那么,await()
就会挂起当前的协程执行流程,等待 Deferred 任务执行完毕,再恢复执行后面剩下的代码。
/**
* 在不阻塞线程的情况下等待该值的完成,并在延迟的计算完成时恢复,
* 返回结果值,如果取消了延迟,则抛出相应的异常。
*
* 这个暂停功能是可以取消的。
* 如果当前协程的[Job]在此挂起函数等待时被取消或完成,则此函数
* 立即恢复[CancellationException]。
* 有**立即取消的保证**。如果在此函数被取消时作业被取消
* 挂起后,它将无法成功恢复。有关底层细节,请参阅[suspendCancellableCoroutine]文档。
*
* 这个函数可以在[select]调用和[onAwait]子句中使用。
* 使用[isCompleted]检查这个延迟值是否已经完成,无需等待。
*/
public suspend fun await(): T
案例:
如果需要通过 async 协程构建器 启动多个并行子协程 , 后面的子协程需要等待前面的子协程执行完毕 , 在启动靠后的子协程 , 实现方案如下 :
private fun runBlockingTest3() {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
runBlocking(block={
// 在 runBlocking 代码块中 , 可以 直接调用 CoroutineScope 的扩展方法 , 如 launch , async 函数 ;
val launchJob1:Job = this@runBlocking.launch{
// // 调用该挂起函数延迟 500 ms
delay(500)
Log.e(TAG,"launchJob1 执行")
"方明飞1" //这个值不会返回
}
Log.e(TAG,launchJob1.toString() ) //todo 协程没有返回结果值 StandaloneCoroutine{Active}@2546022
// todo 挂起父协程 , 等待launchJob1子协程执行完毕会后再执行后面的其他子协程任务
launchJob1.join() //要等子协程launchJob1执行完后,才会执行后面的子协程job2 job3
val launchJob2:Job =this@runBlocking.launch{
// 调用该挂起函数延迟 400 ms
delay(400)
Log.e(TAG,"launchJob2 执行")
"方明飞2"
}
Log.e(TAG,launchJob2.toString() ) //todo 协程没有返回结果值 StandaloneCoroutine{Active}@f837ab3
// todo 挂起父协程 , 等待launchJob2子协程执行完毕会后再执行后面的其他子协程任务
launchJob2.join() //要等子协程launchJob2执行完后,才会执行后面的子协程job2 job3
val asyncJob3 : Deferred<String> = this@runBlocking.async{
// // 调用该挂起函数延迟 300 ms
delay(300)
Log.e(TAG,"asyncJob3 执行")
"方明飞3"
}
//挂起父协程 , 等待asyncJob3子协程执行完毕会后再执行后面的子协程任务 获取 asyncJob3 协程返回值
val asyncJob3_result = asyncJob3.await()
Log.e(TAG, "输出 asyncJob3 协程返回值 : ${asyncJob3_result}") //todo 输出 asyncJob3 协程返回值 : 方明飞3
val asyncJob4 : Deferred<Int> = this@runBlocking.async{
// // 调用该挂起函数延迟 200 ms
delay(200)
Log.e(TAG,"asyncJob4 执行")
1987
}
//挂起父协程 , 等待asyncJob4子协程执行完毕会后再执行后面的子协程任务 获取 asyncJob4 协程返回值
val asyncJob4_result = asyncJob4.await()
Log.e(TAG, "输出 asyncJob4 协程返回值 : ${asyncJob4_result}") //todo 输出 asyncJob3 协程返回值 : 1987
val asyncJob5 : Deferred<Boolean> = this@runBlocking.async{
// // 调用该挂起函数延迟 100 ms
delay(100)
Log.e(TAG,"asyncJob5 执行")
true
}
Log.e(TAG, "输出 asyncJob5 协程返回值 : ${asyncJob5.await()}") //todo 输出 asyncJob5 协程返回值 : true
})
/**
todo 输出各子协程内容的顺序:
launchJob1 执行
launchJob2 执行
asyncJob3 执行
输出 asyncJob3 协程返回值 : 方明飞3
asyncJob4 执行
输出 asyncJob4 协程返回值 : 1987
asyncJob5 执行
输出 asyncJob5 协程返回值 : true
*/
}
协程/Job的生命周期
协程生命周期几个状态符的说明
State | 说明 | [isActive] | [isCompleted] | [isCancelled] |
New (optional initial state) | 新创建 | false | false | false |
Active (default initial state) | 活跃 通过调用 Job#isActivity 获取当前是否处于 活跃状态 ; | true | false | false |
Completing (transient state) | 完成中 | true | false | false |
Cancelling (transient state) | 取消中 | false | false | true |
Cancelled (final state) | 已取消 通过调用 Job#isCancelled 获取当前是否处于 取消状态 ; | false | true | true |
Completed (final state) | 已完成 通过调用 Job#isCompleted 获取当前是否处于 已完成状态 ; | false | true | false |
协程生命周期状态改变
协程 调度执行 后 会变成 活跃 Active 状态 --->
处于活跃状态的协程 有两个分支 , 分别是 协程完成分支 和 协程取消分支 :
--->协程完成分支 : 当有 子协程 完成时 , 会进入 完成中 Completing 状态 , 此时会等待其它子协程执行完毕 , 如果 所有的子协程都执行完毕 , 则进入 已完成 Completed 状态 ;
-->协程取消分支 : 调用 Job#cancel() 函数 取消协程 , 会进入到 取消中 Canceling 状态 , 此时不会立刻取消 , 因为该协程中可能还有多个子协程 , 需要等待 所有子协程都取消后 , 才能进入 已取消 Cancelled 状态 ;
通过一个案例说明协程Job 生命周期的状态变化注意事项:
协程job生命周期状态变化注意事项:
❶如果协程launch(返回Job)是以LAZY懒加载
的方式创建的,那么它的初始状态是New(
isActive=false isCancelled=false isCompleted=false),
协程不会立即执行❷使用 LAZY 作为启动模式,调用 start() 后,活跃状态Active(isActive=true isCancelled=false isCompleted=false)
注意另一种情况: 【如果一个协程是以非懒加载的方式创建的,那么它的初始状态直接是Active (isActive=true isCancelled=false isCompleted=false)】
❸在协程任务正常执行完毕之前,调用cancel()取消之后的状态,最终的
isCancelled=true
和isCompleted
=true都是 true①在协程任务正常执行完毕之前,调用cancel()取消之后的状态: isActive=false isCancelled=true isCompleted=false(isCompleted 状态的更改有一定的延迟) ② 延迟50L,再看看协程job的生命周期状态: 活跃状态isActive=false,取消状态isCancelled=true,完成状态isCompleted=true (延迟50L,isCompleted状态更新为true)
❹在协程任务正常执行完毕之后,
当前协程的生命周期变化状态
Completed:
活跃状态isActive=false,取消状态isCancelled=false,完成状态isCompleted=true.
此时再调用 cancel() 也不会改变协程的状态
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
runBlocking {
val job: Job = launch(start = CoroutineStart.LAZY, block = {
val name = "${Thread.currentThread().name}"
Log.e(TAG, " $name 协程:执行开始")
delay(2000L)
Log.e(TAG, " $name 协程:执行结束")
})
delay(50L) // 使用 LAZY 作为启动模式,初始状态是 New(isActive=false isCancelled=false isCompleted=false)
job.log("协程job第${1}步状态:LAZY懒加载")
job.start() // 使用 LAZY 作为启动模式,调用 start() 后,活跃状态Active(isActive=true isCancelled=false isCompleted=false)
job.log("协程job第${2}步start()后的状态")
delay(50L)
job.cancel() //在协程任务正常执行完毕之前,调用cancel()取消之后的状态:isActive=false isCancelled=true isCompleted=false(isCompleted 状态的更改有一定的延迟)
job.log("在协程任务正常执行完毕之前,协程job第${3}步cancel()后的状态")
delay(50L) //延迟50L,再看看协程job的生命周期状态:活跃状态isActive=false,取消状态isCancelled=true,完成状态isCompleted=true (延迟50L,isCompleted状态更新为true)
job.log("延迟50L,再看看协程job的生命周期状态")
}
/** 协程job第1步状态:LAZY懒加载----->活跃状态isActive=false,取消状态isCancelled=false,完成状态isCompleted=false ----->协程job第1步状态:LAZY懒加载
* 协程job第2步start()后的状态----->活跃状态isActive=true,取消状态isCancelled=false,完成状态isCompleted=false ----->协程job第2步start()后的状态
* main 协程:执行开始
* 在协程任务正常执行完毕之前,协程job第3步cancel()后的状态----->活跃状态isActive=false,取消状态isCancelled=true,完成状态isCompleted=false ----->在协程任务正常执行完毕之前,协程job第3步cancel()后的状态
* 延迟50L,再看看协程job的生命周期状态----->活跃状态isActive=false,取消状态isCancelled=true,完成状态isCompleted=true ----->延迟50L,再看看协程job的生命周期状态
* */
}
}
通过join()等待和invokeOnCompletion {}
监听协程生命结束事件:
如果 Job 内部 delay 时间很长,打印 Process end
之后,程序并不会立即结束,而是等 Job 任务执行完毕以后才真正退出。
为了更加灵活地等待和监听协程的结束事件,我们可以用 join()
以及 invokeOnCompletion {}
优化上面的代码。
invokeOnCompletion {}
的作用是监听协程结束
的事件,如果 job 被取消了,这个回调仍然会被调用
join()
是一个挂起函数
,它的作用是:挂起当前的程序执行流程,待 job 中的协程任务执行完毕后,再恢复当前的程序执行流程
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
runBlocking {
val job: Job = launch(start = CoroutineStart.LAZY, block = {
val name = "${Thread.currentThread().name}"
Log.e(TAG, " $name 协程:执行开始")
delay(2000L)
Log.e(TAG, " $name 协程:执行结束")
})
delay(50L) // 使用 LAZY 作为启动模式,初始状态是 New(isActive=false isCancelled=false isCompleted=false)
job.log("协程job第${1}步状态:LAZY懒加载")
job.start() // 使用 LAZY 作为启动模式,调用 start() 后,活跃状态Active(isActive=true isCancelled=false isCompleted=false)
job.log("协程job第${2}步start()后的状态")
job.invokeOnCompletion(handler={
job.log("第${3}步:监听协程job结束事件")
})
job.join() // 等待协程执行完毕
job.log("第${4}步:协程job调用join()后看看协程job的生命周期状态")
}
/**
* 协程job第1步状态:LAZY懒加载----->活跃状态isActive=false,取消状态isCancelled=false,完成状态isCompleted=false ----->协程job第1步状态:LAZY懒加载
* 协程job第2步start()后的状态----->活跃状态isActive=true,取消状态isCancelled=false,完成状态isCompleted=false ----->协程job第2步start()后的状态
* main 协程:执行开始
* main 协程:执行结束
* 第3步:监听协程job结束事件----->活跃状态isActive=false,取消状态isCancelled=false,完成状态isCompleted=true ----->第3步:监听协程job结束事件
* 第4步:协程job调用join()后看看协程job的生命周期状态----->活跃状态isActive=false,取消状态isCancelled=false,完成状态isCompleted=true ----->第4步:协程job调用join()后看看协程job的生命周期状态
* */
}
}
协程任务泄漏
协程任务泄漏 : 发起 协程任务 后 , 无法追踪任务的执行结果 , 任务等于无效任务 , 但是仍然会消耗 内存 , CPU , 网络 , 磁盘 等资源 ;
Kotlin 中引入了 结构化并发机制 避免 协程任务泄漏 的情况发生 ;
协程任务泄漏 与 内存泄漏 类似 ;
协程结构化并发
Kotlin 协程的结构化并发,是 Kotlin 协程的第二大优势,其重要性仅次于 挂起函数
。
结构化并发,简单来说就是:带有结构和层级的并发。
线程之间是不存在父子
关系的,但协程之间是会存在父子
关系的。Job 源码中有两个 API 是用来描述父子
关系的:
public interface Job : CoroutineContext.Element {
// 一个惰性的集合,可以对它的子 Job 进行遍历
public val children: Sequence<Job>
// 协程内部的 API,用于绑定 ChildJob
@InternalCoroutinesApi
public fun attachChild(child: ChildJob): ChildHandle
}
结构化并发使用场景 :
- 协程任务取消 : 在不需要协程任务的时候 , 取消协程任务 ;
- 追踪协程任务 : 追踪正在执行的协程任务 ;
- 发出错误信号 : 如果 协程任务执行失败 , 发出错误信号 , 表明执行任务出错 ;
协程任务 运行时 , 必须指定其 CoroutineScope 协程作用域 , 其会追踪所有的 协程任务 , CoroutineScope 协程作用域 可以取消 所有由其启动的协程任务 ;
结构化并发的案例:
1.调用 parentJob 的 join()
方法后,它会等待其内部的所有子 Job 全部执行完毕后,parentJob才会恢复执行下面的代码逻辑
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
runBlocking {
val parentJob: Job
var job1: Job? = null
var job2: Job? = null
var job3: Job? = null
// 在外部创建了 1 个父 Job
parentJob = launch {
// 在内部创建了 3 个子 Job
job1 = launch {
Log.e(TAG,"job1 start")
delay(100L)
Log.e(TAG,"job1 end")
}
job2 = launch {
Log.e(TAG,"job2 start")
delay(2000L)
Log.e(TAG,"job2 end")
}
job3 = launch {
Log.e(TAG,"job3 start")
delay(5000L)
Log.e(TAG,"job3 end")
}
}
delay(50L) // 确保所有子 Job 已正常启动,且尚未结束(否则下面的遍历会错误)
parentJob.children.forEachIndexed { index, job -> // 遍历 parentJob 的子 Job
when (index) {
0 -> Log.e(TAG,"children job is job1: ${job1 === job}") // 判断引用是否相等,即是否是同一个对象,结果为 true
1 -> Log.e(TAG,"children job is job2: ${job2 === job}")
2 -> Log.e(TAG,"children job is job3: ${job3 === job}")
}
}
//调用 parentJob 的 join() 方法后,它会等待其内部的子 Job 全部执行完毕,才会恢复执行下面的代码
parentJob.join()
Log.e(TAG,"Process end!")
}
/**
* job1 start
* job2 start
* job3 start
* children job is job1: true
* children job is job2: true
* children job is job3: true
* job1 end
* job2 end
* job3 end
* Process end!
*
* */
}
}
2.调用 parentJob 的 cancel()
方法后,它内部的所有的子协程任务也全都被取消了
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
runBlocking {
val parentJob: Job
var job1: Job? = null
var job2: Job? = null
var job3: Job? = null
// 在外部创建了 1 个父 Job
parentJob = launch {
// 在内部创建了 3 个子 Job
job1 = launch {
Log.e(TAG,"job1 start")
delay(100L)
Log.e(TAG,"job1 end")
}
job2 = launch {
Log.e(TAG,"job2 start")
delay(2000L)
Log.e(TAG,"job2 end")
}
job3 = launch {
Log.e(TAG,"job3 start")
delay(5000L)
Log.e(TAG,"job3 end")
}
}
delay(50L) // 确保所有子 Job 已正常启动,且尚未结束(否则下面的遍历会错误)
//调用 parentJob 的 cancel() 方法后,它内部的所有的子协程任务也全都被取消了
parentJob.cancel()
Log.e(TAG,"Process end!")
}
/**
*
* job1 start
* job2 start
* job3 start
* Process end!
* */
}
}
协程作用域
1.常见的协程作用域
CoroutineScope:
CoroutineScope协程作用域,就是对一个协程定义一个作用域,通过该作用域来启动或者控制协程的生命周期。
协程作用域(CoroutineScope
)是协程运行的作用范围。为了确保所有的协程都会被追踪到,Kotlin 不允许在没有使用CoroutineScope
的情况下启动新的协程。它能启动新的协程,同时这个协程还具备上面所说的suspend
和resume
的优势。
CoroutineScope
定义了新启动的协程作用范围,同时会继承了他的coroutineContext
自动传播其所有的 elements
和取消操作。换句话说,如果这个作用域销毁了,那么里面的协程也随之失效。
因为启动协程需要作用域,但是作用域又是在协程创建过程中产生的,这似乎是一个“先有鸡后有蛋还是先有蛋后有鸡”的问题。
CoroutineScope创建很简单,传入一个CoroutineContext即可。下面是CoroutineScope的源码定义. 这个接口就只包含一个属性CoroutineContext,平时我们在开发使用到的都是CoroutineScope的扩展方法或者扩展属性。比如launch(),async(),cancel(),ensureActive()等,所以在创建CoroutineScope的时候,更重要的是关心CoroutineContenxt。
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
CoroutineScope(Dispatchers.Default).launch {
doSomething()
}
CoroutineScope
也重载了plus
方法,通过+
号来新增或者修改我们CoroutineContext
协程上下文中的Element
。协程作用域本质是一个接口:
public interface CoroutineScope {
//此域的上下文。Context被作用域封装,用于在作用域上扩展的协程构建器的实现。
public val coroutineContext: CoroutineContext
}
public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
ContextScope(coroutineContext + context)
CoroutineScope的缺陷:
GlobalScope
和 CoroutineScope
都是没有绑定到程序的任何生命周期中,也就是说使用这两个方法创建的作用域启动的协程不会在程序或某个页面销毁时自动取消,这在某些情况下可能会造成内存泄漏等问题。
CoroutineScope 最大的作用,就是可以方便我们批量控制协程:
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
runBlocking {
val scope:CoroutineScope = CoroutineScope(Job())
var job1:Job=scope.launch {
log("协程job1 start!")
delay(100L)
log("协程job1 end!")
}
var job2:Job=scope.launch {
log("协程job2 start!")
delay(500L)
log("协程ob2 end!") // 不会执行
}
delay(300L)
scope.cancel()
/**
* 协程job1 start!---所在线程:DefaultDispatcher-worker-1
* 协程job2 start!---所在线程:DefaultDispatcher-worker-2
* 协程job1 end!---所在线程:DefaultDispatcher-worker-1
*
* */
}
}
}
runBlocking
函数: T
runBlocking 函数只有在demo或者测试代码的时候使用,使用 runBlocking 启动的协程会
阻塞
当前线程的执行。请不要在生产环境中使用 runBlocking
顶层函数,创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,返回值是泛型T(
就是你协程体中最后一行是什么类型,最终返回的是什么类型T
就是什么类型。)
,一般在项目中不会使用,主要是为main函数和测试设计的。
我们在普通代码逻辑中或者在main函数中不能直接调用挂起函数,那么我们怎么调试呢? 就是使用runBlocking 函数!
使用 runBlocking 函数,会在普通的代码的主线程中,为我们构建一个新的协程作用域(协程体),
可以运行任何一个suspend挂起函数, 也可以在协程作用域中通过调用 launch ,asnyc挂起函数,开启多个子协程,用于处理协程作用域中的耗时任务。从而调试我们的协程代码。
runBlocking 是桥接阻塞代码与挂起代码之前的桥梁,runBlocking(不是挂起函数)会阻塞当前主线程的,
直到 runBlocking 内部全部子协程(suspend挂起函数)的耗时任务执行完毕,才会继续执行下一步的操作!
runBlocking {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
}
public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T
案例:
private fun runBlockingTest4(){
runBlocking {
// 调用 runBlocking 函数 , 创建一个新的协程作用域
val job1:Job = this@runBlocking.launch{
Log.e(TAG,"job1 子协程开始执行")
// 调用该挂起函数延迟 500 ms
delay(1000)
Log.e(TAG,"job1 子协程执行完毕")
}
coroutineScope {
// 该 coroutineScope 协程作用域 将 子协程 job0 和 job1 包裹起来
// coroutineScope 作用域需要等待 两个子协程执行完毕 , 该作用域才算执行完毕
val job2 = launch {
Log.e(TAG,"job2 子协程开始执行")
delay(10000)
Log.e(TAG, "job2 子协程执行完毕")
}
val job3 = async {
Log.e(TAG,"job3 子协程开始执行")
delay(3000)
Log.e(TAG, "job3 协程执行完毕")
"Hello" // 返回一个字符串
}
delay(5000)
Log.e(TAG, "coroutineScope协程作用域执行完毕")
}
}
}
/**
* job1 子协程开始执行
* job2 子协程开始执行
* job3 子协程开始执行
* job1 子协程执行完毕
* job3 协程执行完毕
* coroutineScope协程作用域执行完毕
* job2 子协程执行完毕
*
*
* */
GlobalScope
:
它启动的协程的生命周期只受整个应用程序的生命周期的限制,且不能取消,在运行时会消耗一些内存资源,这可能会导致内存泄露,所以仍不适用于业务开发。
调用 GlobalScope#launch 方法 , 可以启动一个协程 , 这是顶级的协程(根协程
) , 其 协程作用域是进程级别的 , 生命周期与应用进程同级 , 即使启动协程的对象被销毁或者Activity 被销毁 , 协程任务也可以继续执行 ;
coroutineScope
:
只是一个suspend挂起函数, 只能在协程内或挂起函数内使用。该协程会在另外的独立的线程执行 协程任务 , 不会干扰当前启动协程的线程 ;
它创建一个新的协程作用域,并在该作用域内启动(多个子)协程。 它会等待所有子协程完成后才会继续执行后续代码,才结束自身。coroutineScope主要用于限制子协程的生命周期与父协程相同。
coroutineScope 函数 构建的 协程作用域 , 如果有一个子协程运行失败, 所有其他的子程序都会被取消。为并行分解工作而设计的。
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
案例1:等待所有的子协程执行完
private fun coroutineScopeTest(){
runBlocking {
coroutineScope {
val job1 = launch {
delay(400)
Log.e(TAG, "job1 子协程执行完毕")
}
val job2 = async {
delay(200)
Log.e(TAG, "job2 子协程执行完毕")
"job2 返回值"
}
}
}
}
/**
* job2 子协程执行完毕
* job1 子协程执行完毕
* */
案例2:并发执行两个子协程 , 取消其中一个子协程 , 另外一个子协程也会自动取消 ;
private fun coroutineScopeTest(){
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
runBlocking {
Log.e(TAG, "runBlocking 协程执行了 ${Thread.currentThread().name} ") //main
coroutineScope {
Log.e(TAG, "coroutineScope 协程执行了 ${Thread.currentThread().name} ") //main
// 该 coroutineScope 协程作用域 将 子协程 job0 和 job1 包裹起来
// coroutineScope 作用域需要等待 两个子协程执行完毕 , 该作用域才算执行完毕
// coroutineScope 函数 构建的 协程作用域 ,
// 如果有一个 子协程 执行失败 , 则其它 所有的子协程会被取消 ;
val job0 = launch {
Log.e(TAG, "job0 协程开始执行") //job0 协程开始执行
delay(2000)
Log.e(TAG, "job0 协程执行完毕") // job1 协程 抛出异常取消执行 job0也被取消
}
val job1 = async {
Log.e(TAG, "job1 协程开始执行") //job1 协程开始执行
delay(1000)
// 抛出异常 , job1 执行取消
Log.e(TAG, "job1 协程 抛出异常取消执行") //job1 协程 抛出异常取消执行
throw java.lang.IllegalArgumentException()
Log.e(TAG, "job1 协程执行完毕")
"Hello" // 返回一个字符串
}
Log.e(TAG, "获取子协程 job1 的返回值 ${job1.await()}")
}
}
}
案例3:串行执行多个子协程 ,异常子协程发生时,在异常子协程发生之前的其他子协程会执行,在异常子协程发生之后的其他子协程会被取消,不再执行,
private fun coroutineScopeTest3(){
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
runBlocking {
Log.e(TAG, "runBlocking 协程执行了 ${Thread.currentThread().name} ") //main
coroutineScope {
Log.e(TAG, "coroutineScope 协程执行了 ${Thread.currentThread().name} ") //main
// 该 coroutineScope 协程作用域 将 子协程 job0 和 job1 包裹起来
// coroutineScope 作用域需要等待 两个子协程执行完毕 , 该作用域才算执行完毕
// coroutineScope 函数 构建的 协程作用域 ,
// 如果有一个 子协程 执行失败 , 则其它 所有的子协程会被取消 ;
val job1 = launch {
Log.e(TAG, "job1 协程开始执行") //job1 协程开始执行
delay(500)
Log.e(TAG, "job1 协程执行完毕") //job1 协程执行完毕
}
val job2 = launch {
Log.e(TAG, "job2 协程开始执行") //job2 协程开始执行
delay(2000)
Log.e(TAG, "job2 协程执行完毕") //todo 这行代码不会执行, job4子协程发生异常 导致job2子协程执行被取消
}
val job3 = async {
Log.e(TAG, "job3 协程开始执行") // job3 协程开始执行
delay(1500)
Log.e(TAG, "job3 协程执行完毕") //todo 这行代码不会执行, job4子协程发生异常 导致job3子协程执行被取消
"123"
}
val job4 = async {
Log.e(TAG, "job4 协程开始执行") //job4 协程开始执行
delay(1000)
// 抛出异常 , job4 执行取消
Log.e(TAG, "job4 协程 抛出异常取消执行") //job4 协程 抛出异常取消执行
throw java.lang.IllegalArgumentException() //job4 子协程 抛出异常,自身执行被取消, 导致 所有的其他子协程还未执行的取消执行
Log.e(TAG, "job4 协程执行完毕") //不会输出 job4 子协程 抛出异常,自身执行被取消
"Hello" // 返回一个字符串
}
Log.e(TAG, "获取子协程 job4 的返回值 ${job4.await()}") //不会输出 job4 子协程 抛出异常,自身执行被取消
Log.e(TAG, "获取子协程 job3 的返回值 ${job3.await()}") //不会输出 job4 子协程 抛出异常, 导致 所有的其他子协程(job3)还未执行的取消执行
}
}
}
/**
* runBlocking 协程执行了 main
* coroutineScope 协程执行了 main
job1 协程开始执行
job2 协程开始执行
job3 协程开始执行
job4 协程开始执行
job1 协程执行完毕
job4 协程 抛出异常取消执行
*/
SupervisorJob() 协程:
SupervisorJob 类型的 一个子协程出现异常 , 子协程自己处理异常,不会将 异常传递给 父协程 , 因此也不会影响到CoroutineScope 父协程 下启动的 其它子协程 ;
CoroutineScope(SupervisorJob())在本质上和我们下面讲的
supervisorScope协程
作用域功能一样类似
SupervisorJob()必须要放在CoroutineScope协程作用域的的CoroutineContext上下文中使用才有效 如下:
val scope = CoroutineScope(SupervisorJob()) scope.launch { // Child 1 } scope.launch { // Child 2 }
SupervisorJob 类型的 子协程 自己处理异常 , 不会向上传递异常 ;
Android 使用场景 : 某个 View 组件由 多个协程控制 , 如果其中某个协程崩溃 , 其它协程仍正常工作 ;
Android官方KTX库中的viewModelScope和lifeCycleScope都是使用的SupervisorJob
public val ViewModel.viewModelScope: CoroutineScope{
return setTagIfAbsent( JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)}public val Lifecycle.coroutineScope: LifecycleCoroutineScope{
val newScope = LifecycleCoroutineScopeImpl( this,
SupervisorJob() + Dispatchers.Main.immediate
)}
案例1:正常使用SupervisorJob()参数,调用launch()的正确使用案例:
创建 SupervisorJob 协程 , 需要先 创建一个 协程作用域 , 在 CoroutineScope 构造函数 中 传入
SupervisorJob()
作为参数 ;使用该 协程作用域 调用 launch 构建器函数 , 即可 创建 SupervisorJob 协程 , 这些协程可以自己处理异常 , 不会向父协程传递异常 ;
private fun SupervisorJobTest() {
// 先创建 Supervisor 作用域
// 在该作用域下创建的协程都是 SupervisorJob 协程
val scope : CoroutineScope= CoroutineScope(SupervisorJob())
Log.e(TAG, "当前所处的线程: ${Thread.currentThread().name} ")
// 通过 Supervisor 作用域 创建 协程
val job1 : Job = scope.launch {
delay(100)
Log.e(TAG, "子协程 job1 执行")
Log.e(TAG, "当前所处的线程1: ${Thread.currentThread().name} ")
Log.e(TAG, "获取 协程在上下文指定的调度器1:${coroutineContext}")
}
// 通过 Supervisor 作用域 创建 协程
val job2 : Job= scope.launch {
delay(100)
Log.e(TAG, "子协程 job2 执行")
Log.e(TAG, "当前所处的线程2: ${Thread.currentThread().name} ")
Log.e(TAG, "获取 协程在上下文指定的调度器2:${coroutineContext}")
}
}
/**
* 当前所处的线程: main
* 子协程 job1 执行
* 子协程 job2 执行
* 当前所处的线程1: DefaultDispatcher-worker-1
* 当前所处的线程2: DefaultDispatcher-worker-2
* 获取 协程在上下文指定的调度器1:[StandaloneCoroutine{Active}@ae17401, Dispatchers.Default]
* 获取 协程在上下文指定的调度器2:[StandaloneCoroutine{Active}@d1ce2a6, Dispatchers.Default]
* */
private fun SupervisorJobTest2() {
// 先创建 Supervisor 作用域
// 在该作用域下创建的协程都是 SupervisorJob 协程
val scope : CoroutineScope= CoroutineScope(SupervisorJob())
Log.e(TAG, "当前所处的线程: ${Thread.currentThread().name} ")
// 通过 Supervisor 作用域 创建 协程
val job1 : Job = scope.launch {
delay(100)
Log.e(TAG, "子协程 job1 执行")
supervisorScope {
val job3 : Job = launch {
delay(100)
Log.e(TAG, "子子协程 job3 执行")
}
}
}
// 通过 Supervisor 作用域 创建 协程
val job2 : Job= scope.launch {
delay(100)
Log.e(TAG, "子协程 job2 执行")
supervisorScope {
val job4 : Job = launch {
delay(100)
Log.e(TAG, "子子协程 job4 执行")
}
}
}
}
/**
* 子协程 job1 执行
* 子协程 job2 执行
* 子子协程 job3 执行
* 子子协程 job4 执行
* */
案例2: 使用 SupervisorJob() 子协程launch1发生异常,执行被取消了,使用CoroutineExceptionHandler捕获该子协程launch1的异常并输出 。
子协程 launch2 和 launch3 并不受影响,依然执行。
private fun SupervisorJobTest_CoroutineExceptionHandler(){
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(TAG, "输出协程中捕获的异常 ${throwable.toString()}") //输出协程中捕获的异常 java.lang.ArithmeticException
}
val scope = CoroutineScope(SupervisorJob() + exceptionHandler)
//子协程1 launch1
//给launch()函数传递CoroutineExceptionHandler 捕获该协程中的异常,避免了程序崩溃,程序正常运行
val launch1 = scope.launch(exceptionHandler){
delay(200)
Log.e(TAG,"子协程1 launch1 执行开始")
throw RuntimeException("子协程1 launch1 发生异常")
Log.e(TAG,"子协程1 launch1 执行完毕") //不会输出 发生异常 执行取消了
}
//子协程2 launch2
val launch2 = scope.launch {
delay(300)
Log.e(TAG,"子协程2 launch2 执行完毕")
}
//子协程3 launch3
val launch3 = scope.launch {
delay(400)
Log.e(TAG,"子协程3 launch3 执行完毕")
}
}
/** 使用 SupervisorJob()
* 子协程1 launch1 发生异常,执行被取消了,使用CoroutineExceptionHandler捕获该协程的异常并输出
* 子协程 launch2 和 launch3 并不受影响,依然执行。
*
* 子协程1 launch1 执行开始
* 输出协程中捕获的异常 java.lang.RuntimeException: 子协程1 launch1 发生异常
* 子协程2 launch2 执行完毕
* 子协程3 launch3 执行完毕
* */
案例3:try-catch
包裹 await()
后,可以捕获到异常
,但不能消化掉异常,会导致程序异常崩溃
private fun try_catch_await_test(){
runBlocking {
val deferred = async { 1 / 0 }
try {
// 使用 try-catch 包裹 await() 捕获到了异常,但程序最终还是崩溃了。
deferred.await()
} catch (e: ArithmeticException) {
println("Catch: $e")
}
delay(500L)
Log.e(TAG, "End")
}
}
案例3的解决方案1:
借助 SupervisorJob
, 不要调用 await() 就不会产生异常,不会崩溃。
private fun SupervisorJob_without_await(){
runBlocking {
val scope = CoroutineScope(SupervisorJob()) // 使用 SupervisorJob
scope.async { 1 / 0 } // 因为没有调用 await(),所以不会产生异常
delay(500L)
Log.e(TAG,"press End") // 程序会正常打印输出日志: press End,并且会正常结束
} }
案例3的解决方案2:
借助 SupervisorJob
, try-catch
包裹 await()
后,可以 捕获调用 await() 后产生的异常,不会导致程序异常崩溃
private fun SupervisorJob_with_await(){
runBlocking {
val scope = CoroutineScope(SupervisorJob()) // 使用 SupervisorJob
val deferred =scope.async { 1 / 0 }
try { // 使用 try-catch 包裹 await()
deferred.await() // 可以捕获调用 await() 后产生的异常
} catch (e: ArithmeticException) {
println("异常信息: $e") // 异常信息:java.lang.ArithmeticException: divide by zero
}
delay(500L)
Log.e(TAG,"press End2") // 程序会正常打印输出日志: press End2,并且会正常结束
}
}
Job()
使用Job(), 如果子协程抛出异常,try/catch并不能捕异常
如果存在上一级,异常则会向上一级父协程传递异常,导致父协程执行取消。 最终还是导致程序崩溃。
同时其他的子协程的执行也会取消。
案例1:
使用Job方式,
孙协程deferred发生异常,执行取消,try/catch并不能捕异到孙协程异常.
孙协程deferred的异常 向上传递给 父协程(子协程launch1) 导致父协程(子协程launch1)执行取消, 程序会崩溃.
其他的子协程 执行也会取消.
private fun JobTest() {
val scope = CoroutineScope(Job() + Dispatchers.IO)
//子协程1 launch1
val launch1 = scope.launch {
Log.e(TAG,"子协程1 launch1 执行开始")
try {
//孙协程deferred
val deferred = async {
throw ArithmeticException("孙协程deferred发生异常")
}
deferred.await()
} catch (ex: Exception) {
//async中的异常不会到达这里,会把异常向上传递
ex.printStackTrace()
Log.e(TAG, "孙协程deferred抛出异常:${ex.toString()}") // 孙协程deferred抛出异常:java.lang.ArithmeticException
}
delay(200)
Log.e(TAG,"子协程1 launch1 执行完毕")
}
//子协程2 launch2
val launch2 = scope.launch {
delay(300)
Log.e(TAG,"子协程2 launch2 执行完毕")
}
//子协程3 launch3
val launch3 = scope.launch {
delay(400)
Log.e(TAG,"子协程3 launch3 执行完毕")
}
}
/** 使用Job方式,
* 孙协程deferred发生异常,执行取消,try/catch并不能捕异到孙协程异常
* 孙协程deferred的异常 向上传递给 父协程(子协程launch1) 导致父协程(子协程launch1)执行取消, 程序会崩溃.
其他的子协程 执行也会取消
子协程1 launch1 执行开始
* 孙协程deferred抛出异常:java.lang.ArithmeticException: 孙协程deferred发生异常
* */
解决案例1:
给launch()函数传递CoroutineExceptionHandler 捕获协程中的异常,避免了程序崩溃,程序正常运行
private fun JobTest_ExceptionHandler() {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(TAG, "输出协程中捕获的异常 ${throwable.toString()}") //输出协程中捕获的异常 java.lang.ArithmeticException
}
val scope = CoroutineScope(Job() + Dispatchers.IO)
//给launch()函数传递CoroutineExceptionHandler 捕获协程中的异常,避免了程序崩溃,程序正常运行
scope.launch(exceptionHandler) {
val deferred = async {
throw ArithmeticException()
}
deferred.await()
}
}
/**
* 输出协程中捕获的异常 java.lang.ArithmeticException
* 给launch()函数传递CoroutineExceptionHandler 捕获协程中的异常,避免了程序崩溃,程序正常运行
* */
解决方案2:
给协程函数CoroutineScope()的参数上下文CouroutineContext,传递CoroutineExceptionHandler 捕获协程中的异常,避免了程序崩溃,程序正常运行
private fun JobTest_ExceptionHandler2() {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(TAG, "输出协程中捕获的异常 ${throwable.toString()}") //输出协程中捕获的异常 java.lang.ArithmeticException
}
//给协程函数CoroutineScope()的参数上下文CouroutineContext,传递CoroutineExceptionHandler 捕获协程中的异常,避免了程序崩溃,程序正常运行
val scope = CoroutineScope(Job() + Dispatchers.IO+exceptionHandler)
scope.launch{
val deferred = async {
throw ArithmeticException()
}
deferred.await()
}
}
/**
* 给协程函数CoroutineScope()的参数上下文CouroutineContext,传递CoroutineExceptionHandler 捕获协程中的异常,避免了程序崩溃,程序正常运行
* 给launch()函数传递CoroutineExceptionHandler 捕获协程中的异常,避免了程序崩溃,程序正常运行
* */
Job()和SupervisorJob()的区别
区别一:主要从二者的定义和源码来分析区别
Job():
返回的是JobImpl对象。JobImpl继承自JobSupport, 重写了childCancelled方法,返回值为true。
而JobSupport会调用cancelImpl方法,该方法的作用是取消父Job和父Job的所有子Job。
public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)
SupervisorJob():
返回的SupervisorJobImpl对象。 SupervisorJobImpl 重写了childCancelled方法,返回值为false。表示父Job不会因为子Job被取消而跟随取消。
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
override fun childCancelled(cause: Throwable): Boolean = false
}
区别二:主要从二者的对子协程发生异常的处理不同的区别:
异常的处理逻辑可以用职场的例子解释。假设职场的潜规则是,任何员工出错了,首要是要向上级报告,
如果上级愿意处理你的错误,那员工就不用管了,如果上级将问题打回给员工,那错误就得由员工自己处理。那么回到问题本身,Job就相当于一个好老板,子协程犯的错,好老板(Job)愿意处理,员工(子协程)不用处理这个错误了
SupervisorJob就相当于一个严厉的老板,严厉老板(SupervisorJob)不管不处理, 子协程自己犯的错,自己解决。严厉老板(SupervisorJob),其他员工(子协程)也不受这个错误(异常)影响。
Job():
有Job类型启动的协程时,如果子协程发生异常,异常会交由根(父)协程处理。
private fun JobTest_ExceptionHandler3(){
val parentExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(TAG, "父协程捕获处理异常: ${throwable.toString()}")
}
val childExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(TAG, "子协程捕获处理异常: ${throwable.toString()}")
}
MainScope().launch(parentExceptionHandler) {
val childLaunch = this.launch(Job(coroutineContext[Job])+childExceptionHandler){
Log.e(TAG, "获取 子协程childLaunch上下文信息:${this.coroutineContext}")
delay(100)
throw ArithmeticException("子协程空指针异常")
}
}}
/**
* 获取 子协程childLaunch上下文信息:[ MainActivity$JobTest_ExceptionHandler3$$inlined$CoroutineExceptionHandler$2@6f66211,
* StandaloneCoroutine{Active}@2ef1f76, Dispatchers.Main]
* 父协程捕获处理异常: java.lang.ArithmeticException: 子协程空指针异常
*
* */
SupervisorJob():
有SupervisorJob类型启动的协程时,如果子协程发生异常,异常会交由子协程自己处理。
private fun SupervisorJobTest_CoroutineExceptionHandler2(){
val parentExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(TAG, "父协程捕获处理异常: ${throwable.toString()}")
}
val childExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(TAG, "子协程捕获处理异常: ${throwable.toString()}")
}
MainScope().launch(parentExceptionHandler) {
val childLaunch = this.launch(SupervisorJob(coroutineContext[Job]) +childExceptionHandler){
Log.e(TAG, "获取 子协程childLaunch上下文信息:${this.coroutineContext}")
delay(100)
throw ArithmeticException("子协程空指针异常")
}
}
}
/**
* 获取 子协程childLaunch上下文信息:[MainActivity$SupervisorJobTest_CoroutineExceptionHandler2$$inlined$CoroutineExceptionHandler$2@6f66211,
* StandaloneCoroutine{Active}@2ef1f76, Dispatchers.Main]
* 子协程捕获处理异常: java.lang.ArithmeticException: 子协程空指针异常
*
* */
supervisorScope
:
使用supervisorScope 函数 构建的 协程作用域 , 如果有一个 子协程 执行失败 , 其它子协程继续执行 , 不会受到执行失败的子协程影响 ; 不会影响父协程(这个作用域)的运行。
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R
案例1: 并发执行两个协程 , 取消其中一个协程 , 另外一个协程不会受到影响 , 仍然执行完毕 ;
private fun supervisorScopeTest1(){
runBlocking {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
supervisorScope {
// supervisorScope 函数 构建的 协程作用域 ,
// 如果有一个 子协程 执行失败 ,
// 其它子协程继续执行 , 不会受到执行失败的子协程影响 ;
val job0 = launch {
Log.e(TAG, "job0 协程开始执行") //job0 协程开始执行
delay(2000)
Log.e(TAG, "job0 协程执行完毕") //job0 协程执行完毕
}
val job1 = async {
Log.e(TAG, "job1 协程开始执行") //job1 协程开始执行
delay(1000)
// 抛出异常 , job1子协程 执行取消
Log.e(TAG, "job1子 协程 抛出异常取消执行") //job1子 协程 抛出异常取消执行
throw java.lang.IllegalArgumentException()
Log.e(TAG, "job1 协程执行完毕") //不会输出 job1 子协程 抛出异常,自身执行被取消
"Hello" // 返回一个字符串 无法输出
}
}
}
}
/**
* job0 协程开始执行
* job1 协程开始执行
*job1子 协程 抛出异常取消执行
* job0 协程执行完毕
*
* */
MainScope(
SupervisorJob() +Dispatchers.Main
)(重点)
:
定义:
为UI组件创建主作用域。是一个顶层函数,上下文是SupervisorJob() + Dispatchers.Main
,说明它是一个在主线程执行的协程作用域,通过cancel
对协程进行取消。推荐使用。
MainScope
作用域的好处就是方便地绑定到UI组件的声明周期上,该 作用域仅在 Activty 中 ,在Activity的 onDestory 生命周期销毁时,mainScope.cancel()
取消协程任务。
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
MainScope 是一个 函数 , 其返回值类型为 CoroutineScope 协程作用域 ; 这是使用了设计模式中的 工厂模式 , 生产一个 协程作用域 实例对象 ; 之后的 协程操作都要定义在该协程作用域中 ;
MainScope 协程作用域 与之前使用的 GlobalScope 协程作用域 作用相同 , 执行 lunch 函数 , 后面的代码块就是协程作用域 , 在其中执行协程任务 ;
CoroutineScope.launch 函数 是 协程作用域的扩展函数 , 其后的代码块参数就是 协程作用域 , 在其中执行协程任务 ;
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
}
取消 MainScope 协程作用域:
调用 MainScope 协程作用域 的 cancel 函数 , 即可取 消该 协程作用域 , 同时 该协程作用域内的协程任务不管是否执行完毕 都一并取消 , 该函数是 CoroutineScope 的扩展函数 ;
/**
* 取消这个范围,包括它的作业和它的所有子任务,可选的取消[原因]。
* 原因可以用来指定错误消息或提供其他细节为调试目的而取消的原因。
* 如果作用域中没有作业,抛出[IllegalStateException]。
* /
public fun CoroutineScope.cancel(cause: CancellationException? = null) {
val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
job.cancel(cause)
}
只要是通过该 private val mainScope = MainScope()
协程作用域 启动的协程任务 , 如果取消 mainScope
协程作用域 , 则在该 协程作用域 中执行的 协程任务 , 都会被取消 ;
挂起函数中途被取消 会抛出 JobCancellationException 异常 , 异常信息如下 :
kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelling}@57a393b
在 Activity 的 onDestroy 生命周期 函数中 , 取消 协程作用域 ;
override fun onDestroy() {
super.onDestroy()
// 在 Activity 销毁前取消协程作用域
mainScope.cancel()
}
案例:
class MainActivity : ComponentActivity() {
val tv_title: TextView by lazy<TextView>{
findViewById<TextView>(R.id.tv_title)
}
/**
* 协程作用域
* 该 作用域仅在 Activty 中 , 如果 Activity 被销毁 ,
* 则 在 onDestory 生命周期函数中取消协程任务 ;
*/
val mainScope = MainScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
MainScopeTest()
}
private fun MainScopeTest() {
mainScope.launch { // 协程作用域, 在该代码块中执行协程任务
// 通过suspend withContext(Dispatchers.IO)挂起当前协程,使用Dispatchers.IO协程任务调度器, 用于执行耗时操作做网络请求任务
val result: User = withContext(context=Dispatchers.IO,
block= {
Log.e(TAG,"withContext : 协程中执行耗时操作")
mainViewModel.getUserSuspend()
})
try{
// 挂起函数, 可以不使用协程调度器
delay(20000)
}catch (e : Exception){
Log.e(TAG, "中断挂起函数任务, 报异常:")
e.printStackTrace()
}
// 挂起恢复,继续执行当前协程的后面的任务,进行UI的更新
tv_title.text=result.toString()
}
}
override fun onDestroy() {
super.onDestroy()
// 在 Activity 销毁前取消协程
mainScope.cancel()
}
}
Activity 实现 CoroutineScope 协程作用域接口:
通过委托方式 , Activity继承 CoroutineScope 接口 ,即可 将整个 协程作用域 委托给 Activity ,在 Activity 中可以 直接调用 launch 函数执行协程任务 , 调用 cancel 函数取消协程作用域 ;
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
class MainActivity : AppCompatActivity(), CoroutineScope by MainScope()
2.Lifecycle的协程支持
lifecycleScope
(Dispatchers.Main
):
lifecycleScope
绑定了 Activity
/ Fragment
的生命周期,当 Activity
/ Fragment
生命周期结束时,lifecycleScope
协程也会被取消
Lifecycle Ktx
库提供的具有生命周期感知的协程作用域,与Lifecycle
绑定生命周期,生命周期被销毁时,此作用域将被取消。会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,推荐使用。
Android 官方也利用 Kotlin 扩展属性的方式给 Activity
等具有生命周期的组件提供了开启协程所需的 CoroutineScope
(因为Activity
实现了LifecycleOwner
这个接口,而lifecycleScope协程
则正是LifecycleOwner
的拓展成员属性,可以在Activity中可以直接使用lifecycleScope
协程实例),其中的 context 指定了使用 Dispatchers.Main
,即通过 lifecycleScope
开启的协程默认都会被调度到主线程执行。
因此当我们在调用一个 suspend
函数做耗时请求操作时,拿到结果数据后可以直接更新 UI,无须做任何线程切换的动作。这样的 suspend
函数叫作「main 安全」的。
package androidx.lifecycle
import kotlinx.coroutines.CoroutineScope
public interface LifecycleOwner {
public val lifecycle: Lifecycle
}
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
lifecycleScope.launch{
Log.e(TAG, "当前所处的线程: ${Thread.currentThread().name} ") //当前所处的线程: main
Log.e(TAG, "获取 协程在上下文指定的调度器:${this.coroutineContext}") // 获取 协程在上下文指定的调度器:[StandaloneCoroutine{Active}@83870ed, Dispatchers.Main.immediate]
val postResult = retrofit.get<PostService>().fetchPosts();
// 由于在主线程,可以拿着 postResult结果 直接更新 UI
findViewById<TextView>(R.id.textview).text = postResult
}}}
比如下面这个代码块,我们指定这个协程块调度到主线程执行,里面调用了一个不知道哪里来的 suspend foo()
方法。这个方法内部可能是耗时的 CPU 计算,可能是耗时的 IO 请求,但是我在写这个协程块的时候,其实并不需要关心这里面到底是怎么回事,运行在哪个线程。类似地,在阅读这段协程块的时候,我们可以清楚地知道眼前的这段代码会在主线程执行,suspend foo()
里面的代码是一个潜在的耗时操作,具体在哪个线程执行是这个函数的实现细节,对于当前代码的逻辑是「透明」的。
suspend fun foo() 错误的没有遵守suspend的语义,会阻塞主线程UI
suspend fun foo()函数 内部实现是一段非常耗时的CPU 计算 操作代码,当我们调用foo()函数,会严重阻碍主线程UI的初始化运行
并不是单纯地给foo()函数加上 suspend 关键字就让foo()函数变成非阻塞(主线程UI)的,foo()函数没有遵守suspend的语义,是错误的
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
lifecycleScope.launch(context=Dispatchers.Main,
block={
Log.e(TAG, "当前所处的线程1: ${Thread.currentThread().name} ") //当前所处的线程: main
Log.e(TAG, "获取 协程在上下文指定的调度器1:${this.coroutineContext}") //获取 协程在上下文指定的调度器:[StandaloneCoroutine{Active}@8872fef, Dispatchers.Main]
val bigIntegerResult:BigInteger=foo() //耗时请求操作,会阻塞主线程UI
Log.e(TAG, "输出bigIntegerResult=$bigIntegerResult")
})
}
/** suspend fun foo() 错误的没有遵守suspend的语义,会阻塞主线程UI
* suspend fun foo()函数 内部实现是一段非常耗时的CPU 计算 操作代码,当我们调用foo()函数,会严重阻碍主线程UI的初始化运行
* 并不是单纯地给foo()函数加上 suspend 关键字就让foo()函数变成非阻塞(主线程UI)的,foo()函数没有遵守suspend的语义,是错误的
*
* */
suspend fun foo():BigInteger = BigInteger.probablePrime(4096, Random())
}
改进正确的做法:
借助 withContext 我们把BigInteger.probablePrime()的耗时CPU 计算操作 从当前主线程(切换到一个子线程里去)挪到了一个默认的后台线程池。不会阻塞当前的主线程,主线程能够进行其他的初始化操作
当耗时操作完成计算出结果数据后,再在主线程更新显示请求的数据
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
lifecycleScope.launch(context=Dispatchers.Main,
block={
Log.e(TAG, "当前所处的线程1: ${Thread.currentThread().name} ") //当前所处的线程1: main
Log.e(TAG, "获取 协程在上下文指定的调度器1:${this.coroutineContext}") //获取 协程在上下文指定的调度器1:[StandaloneCoroutine{Active}@8872fef, Dispatchers.Main]
val bigIntegerResult:BigInteger=findBigPrime()
Log.e(TAG, "输出bigIntegerResult=$bigIntegerResult")
Log.e(TAG, "当前所处的线程3: ${Thread.currentThread().name} ") //当前所处的线程3: main
Log.e(TAG, "获取 协程在上下文指定的调度器3:${this.coroutineContext}") //获取 协程在上下文指定的调度器3:[StandaloneCoroutine{Active}@8872fef, Dispatchers.Main]
//在主线程更新显示请求的数据bigIntegerResult
findViewById<TextView>(R.id.textview).text= bigIntegerResult.toString()
})
}
/** 改进正确的做法:
* 借助 withContext 我们把BigInteger.probablePrime()的耗时CPU 计算操作 从当前主线程(切换到一个子线程里去)挪到了一个默认的后台线程池。不会阻塞当前的主线程,主线程能够进行其他的初始化操作
* 当耗时操作完成计算出结果数据后,再在主线程更新显示请求的数据
*
* */
suspend fun findBigPrime():BigInteger{
val bigIntegerResult:BigInteger=withContext(context=Dispatchers.Default,block={
Log.e(TAG, "当前所处的线程2: ${Thread.currentThread().name} ") //当前所处的线程2: DefaultDispatcher-worker-1
Log.e(TAG, "获取 协程在上下文指定的调度器2:${this.coroutineContext}") //获取 协程在上下文指定的调度器2:[DispatchedCoroutine{Active}@4046afc, Dispatchers.Default]
BigInteger.probablePrime(4096, Random()) //lambda最后一行结果值作为返回值
})
return bigIntegerResult
}
}
viewModelScope
(Dispatchers.Main
):
与ViewModel
绑定生命周期,当ViewModel
被清除时,这个作用域将被取消。推荐使用。
VIewModel 的作用域会在它的 clear 函数调用时取消。
在特定界面中 , 如可旋转屏幕的 Activity 界面中 , 如果使用 MainScope 协程作用域 , 当屏幕旋转时 , 就会在 onDestory 生命周期函数中 取消协程作用域 , 此时协程相关的临时数据都被取消了 ;
当旋转 Activity 界面时 , 会调用当前 Activity 的 onDestory 生命周期函数 , 自然对应的协程作用域也会被取消 , 因此引入 viewModelScope 作用域 , 避免协程临时数据被销毁 ;
案例:
data class Student(val name: String, val age: Int)
class MainViewModel:ViewModel() {
// 在布局文件中配置的属性
private val _student: MutableLiveData<Student> = MutableLiveData<Student>()
val student: LiveData<Student> = _student
// 该方法用于刷新数据
fun setStudentData( name: String, age: Int){
viewModelScope.launch {
withContext(context= Dispatchers.IO,
block= { //切换到IO线程 网络请求
//在协程中使用 delay 函数 , 挂起 10 秒时间 , 然后 10 秒后更新 UI ;
delay(10000)
_student.postValue(Student(name, age))
})
}
}
}
class MainActivity : ComponentActivity() {
private val mainViewModel :MainViewModel by viewModels<MainViewModel>()
val tv_title: TextView by lazy<TextView>{
findViewById<TextView>(R.id.tv_title)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModelScopeTest()
}
private fun viewModelScopeTest() {
mainViewModel.setStudentData("Tom", 18)
mainViewModel.student.observe(this){
tv_title.text=it.toString() //Student(name="Tom",age=18)
}
}
3.协程的分类和行为规则
顶级作用域:
没有父协程的协程所在的作用域为顶级作用域。
协同作用域:
在协程中启动一个新协程,新协程为所在协程的子协程。子协程所在的作用域默认为协同作用域。此时子协程抛出未捕获的异常时,会将异常传递给父协程处理,如果父协程被取消,则所有子协程同时也会被取消。
主从作用域:
该作用域下的 子协程出现异常,不会导致其它子协程取消。但是如果父协程被取消,则所有子协程同时也会被取消。
同时补充一点:
(1)父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。
(2)父协程需要等待所有的子协程执行完毕之后才会进入完成
Completed
状态,不管父协程自身的协程体是否已经执行完成。我们在最开始提到协程生命周期的时候就提到过下,现在回过头看是不是感觉很流程变得清晰。(3)子协程会继承父协程的协程上下文中的
Element
,如果自身有相同key的成员,则覆盖对应的key
,覆盖的效果仅限自身范围内有效。这个就可以用上我们前面学到的协程上下文CoroutineContext
的知识,小案例奉上:
private fun GlobalScopeTest() {
//创建一个根协程
GlobalScope.launch(Dispatchers.Main) {//父协程
Log.e(TAG, "GlobalScope父协程上下文=$coroutineContext")
//GlobalScope父协程上下文=[StandaloneCoroutine{Active}@e709305, Dispatchers.Main]
this@launch.launch(CoroutineName("第一个子协程")) {
//获取第一个子协程上下文[CoroutineName(第一个子协程), StandaloneCoroutine{Active}@ba3658b, Dispatchers.Main]
Log.e(TAG, "获取第一个子协程上下文$coroutineContext")
}
this@launch.launch(Dispatchers.Unconfined) {
//获取第二个子协程上下文[StandaloneCoroutine{Active}@d984f5a, Dispatchers.Unconfined]
Log.e(TAG, "获取第二个子协程上下文$coroutineContext")
}
}
}
D/父协程上下文: [StandaloneCoroutine{Active}@81b6e46, Dispatchers.Main]
D/第二个子协程协程上下文: [StandaloneCoroutine{Active}@f6b7807, Dispatchers.Unconfined]
D/第一个子协程上下文: [CoroutineName(第一个子协程), StandaloneCoroutine{Active}@bbe6d34, Dispatchers.Main]
第一个子协程的覆盖了父协程的coroutineContext,它继承了父协程的调度器 Dispatchers.Main,同时也新增了一个CoroutineName属性。第二个子协程覆盖了父协程的coroutineContext中的Dispatchers,也就是将父协程的调度器Dispatchers.Main覆盖为Dispatchers.Unconfined,但是他没有继承第一个子协程的CoroutineName,这就是我们说的覆盖的效果仅限自身范围内有效。
4.用法对比
(1)runBlocking与coroutineScope 与 CoroutineScope的区别
runBlocking:
我们在普通代码逻辑中或者在main函数中不能直接调用挂起函数,那么我们怎么调试呢? 就是使用runBlocking 函数!
使用 runBlocking 函数,会在普通的代码的主线程中,为我们构建一个新的协程作用域(协程体),
可以运行任何一个suspend挂起函数, 也可以在协程作用域中通过调用 launch ,asnyc挂起函数,开启多个子协程,用于处理协程作用域中的耗时任务。从而调试我们的协程代码。
runBlocking 是桥接阻塞代码与挂起代码之前的桥梁,runBlocking(不是挂起函数)会阻塞当前主线程的,
直到 runBlocking 内部全部子协程(suspend挂起函数)的耗时任务执行完毕,才会继续执行下一步的操作!
CoroutineScope: 是一个接口,它定义了一个协程作用域。
通过创建CoroutineScope
的实例对象,我们可以启动和管理协程。CoroutineScope
通常与launch
或async
函数一起使用,用于创建并启动协程。
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
// 创建 CoroutineScope 并包装所给的上下文
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
案例:
fun main() {
CoroutineScope(Dispatchers.Default).launch {
delay(1000L)
println("Task from CoroutineScope")
}
println("CoroutineScope is over")
}
案例:
private fun CoroutineScopeTest2() {
runBlocking {
val coroutineScope :CoroutineScope = CoroutineScope(Dispatchers.Default)
val job1 :Job = coroutineScope.launch {
delay(2000)
Log.e(TAG, "job1 子协程执行完毕")
}
val job2:Job = coroutineScope.launch {
delay(2000)
Log.e(TAG, "job2 子协程执行完毕")
}
val deferred:Deferred<String> = coroutineScope.async {
delay(2000)
Log.e(TAG, "deferred 子协程执行完毕")
"123"
}
val coroutineScope2 :CoroutineScope = CoroutineScope(Dispatchers.Default)
val job3:Job = coroutineScope2.launch {
delay(1000)
Log.e(TAG, "job3 子协程执行完毕")
}
val deferred2:Deferred<String> = coroutineScope2.async {
delay(1000)
Log.e(TAG, "deferred2 子协程执行完毕")
"456"
}
delay(100)
coroutineScope2.cancel() 这里是取消协程作用域coroutineScope2,会取消它关联的所有子协程job3 deferred2
delay(1000)
}
}
/** 只有协程作用域coroutineScope1的子协程job1 job2 deferred 输出
* job1 子协程执行完毕
* job2 子协程执行完毕
* deferred 子协程执行完毕
*
* */
coroutineScope :只是一个suspend挂起函数,它创建一个新的协程作用域并在该作用域内启动(多个子)协程。 它会等待所有子协程完成后才会继续执行后续代码。
coroutineScope
主要用于限制子协程的生命周期与父协程相同。
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
案例:
suspend fun main() {
coroutineScope {
launch {
delay(2000L)
println("Task from coroutine scope")
}
launch {
delay(1000L)
println("Task from coroutine scope")
}
async{
delay(1500L)
println("Task from coroutine scope")
}
println("Coroutine scope is over")
}
}
案例:
private fun coroutineScopeTest(){
runBlocking {
coroutineScope {
val job1 = launch {
delay(400)
Log.e(TAG, "job1 子协程执行完毕")
}
val job2 = async {
delay(200)
Log.e(TAG, "job2 子协程执行完毕")
"job2 返回值"
}
}
}
}
}
/**
* job2 子协程执行完毕
* job1 子协程执行完毕
* */
(2)coroutineScope 与 supervisorScope的区别
当使用coroutineScope
时,如果一个子协程发生异常,那么所有其他子协程将被取消,异常会向上传递到父协程,父协程也会取消。
当使用supervisorScope
时,子协程之间是相互独立的。如果一个子协程发生异常,其他子协程不会受到影响,异常需要在子协程内部处理。
(3)coroutineScope 与 withContext的区别
coroutineScope
用于创建一个新的协程作用域,并在该作用域内启动子协程。它会等待所有子协程完成后才会继续执行后续代码。coroutineScope
主要用于限制子协程的生命周期与父协程相同。
协程调度器 CoroutineDispatcher
其实协程可以简单的理解为对线程的封装,它可以帮我们管理程序在不同的线程上运行。
所以我们启动协程时一般都需要指定协程的调度器,即这个协程中的代码应该在什么线程中去运行。即协程调度器
确定了协程中的代码在哪个线程或哪些线程上执行。或者在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
协程需要调度的位置就是挂起点的位置,只有当挂起点正在挂起的时候才会进行调度,实现调度需要使用协程的拦截器。调度的本质就是解决挂起点恢复之后的协程逻辑代码在哪里(在那个线程)运行的问题。调度器也属于协程上下文一类,它继承自拦截器:
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
//询问调度器是否需要分发
public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
//将可运行块的执行分派到给定上下文中的另一个线程上。这个方法应该保证给定的[block]最终会被调用。
public abstract fun dispatch(context: CoroutineContext, block: Runnable)
//返回一个continuation,它封装了提供的[continuation],拦截了所有的恢复。
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
//CoroutineDispatcher是一个协程上下文元素,而'+'是一个用于协程上下文的集合和操作符。
public operator fun plus(other: CoroutineDispatcher): CoroutineDispatcher = other
}
CoroutineDispatcher
是所有协程调度程序实现扩展的基类(我们很少会自己自定义调度器)。可以使用newSingleThreadContext
和newFixedThreadPoolContext
创建私有线程池。也可以使用asCoroutineDispatcher
扩展函数将任意java.util.concurrent.Executor
转换为调度程序。
1.调度器模式
public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
@JvmStatic
public actual val Main: MainCoroutineDispatcher
get() = MainDispatcherLoader.dispatcher
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}
调度器模式 | 说明 | 适用场景 | 注意 |
| ❶默认调度器,在 子线程 中运行 , 处理 CPU 耗时任务 , 主要侧重算法消耗 ; 当 ❷ | 通常处理一些单纯的计算任务,或者执行时间较短任务比如:数据排序 , 数据解析 , 数据对比 等耗时算法操作 ,Json 的解析,数据计算等。 | 在子线程 中执行耗时任务 |
| UI 调度器, Andorid 上的主线程。处理 UI 交互任务 | 调用 挂起 suspend 函数 , 更新 UI , 更新 LiveData ; | 在协程中调用 挂起 suspend 函数 , 必须在 Dispatchers.Main 调度器中执行 ; |
| 一个不局限于任何特定线程的协程调度程序,即非受限调度器。 | 子协程切换线程代码会运行在原来的线程上,协程在相应的挂起函数使用的任何线程中继续。 | ❶不要使用 Unconfined. ❷Unconfined 其实是很危险的,我们不应该随意使用。他会改变程序的执行顺序 |
| 在 子线程 中运行 , 处理 文件操作 和 网络 IO 操作 ; | 适合执行IO 相关操作,比如:网络处理,数据库操作增删查改 ,文件读写等。 | 在子线程 中执行耗时任务 |
所有的协程构造器(如
launch
和async
)都接受一个可选参数,即CoroutineContext
,该参数可用于显式指定要创建的协程和其它上下文元素所要使用的CoroutineDispatcher
。
private fun dispatchersTest() {
//创建一个在主线程执行的协程作用域
val mainScope = MainScope()
mainScope.launch {
//MainScope父协程的调度器=[StandaloneCoroutine{Active}@ba3658b, Dispatchers.Main]
Log.e(TAG, "MainScope父协程的调度器=$coroutineContext")
this.launch(CoroutineName("第一个子协程")+Dispatchers.Main) { //在协程上下参数中指定调度器Main
//第一个子协程在上下文指定的调度器:[CoroutineName(第一个子协程), StandaloneCoroutine{Active}@d93e114, Dispatchers.Main]
Log.e(TAG, "第一个子协程在上下文指定的调度器:$coroutineContext")
}
this.launch(CoroutineName("第二个子协程")+Dispatchers.Default) { //在协程上下参数中指定调度器Default
//第二个子协程在上下文指定的调度器:[CoroutineName(第二个子协程), StandaloneCoroutine{Active}@204c668, Dispatchers.Default]
Log.e(TAG, "第二个子协程在上下文指定的调度器:$coroutineContext")
}
this.launch(CoroutineName("第三个子协程")+Dispatchers.Unconfined) { //在协程上下参数中指定调度器Unconfined
// 第三个子协程在上下文指定的调度器:[CoroutineName(第三个子协程), StandaloneCoroutine{Active}@cec5a81, Dispatchers.Unconfined]
Log.e(TAG, "第三个子协程在上下文指定的调度器:$coroutineContext")
}
this.launch(CoroutineName("第四个子协程")+Dispatchers.IO) { //在协程上下参数中指定调度器IO
//第四个子协程在上下文指定的调度器:[CoroutineName(第四个子协程), StandaloneCoroutine{Active}@4fc0726, Dispatchers.IO]
Log.e(TAG, "第四个子协程在上下文指定的调度器:$coroutineContext")
}
}
}
案例:自定义线程池
不要使用 Unconfined.
Unconfined
代表的意思是,当前协程可能运行在任何线程之上,不作强制要求。Unconfined 其实是很危险的,我们不应该随意使用。他会改变程序的执行顺序。
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
runBlocking {
val myDispatcher: ExecutorCoroutineDispatcher = Executors.newSingleThreadExecutor {
Thread(it, "我的自定义的线程池bqt").apply { isDaemon = true }
}.asCoroutineDispatcher() //自定义一个CoroutineContext上下文
log(1) // 运行在当前线程
withContext(myDispatcher) { log(2) } // 自定义的线程池
withContext(Dispatchers.IO) { log(3) } // IO 任务线程池
withContext(Dispatchers.Unconfined) { log(4) } // 无限制的线程池
}
/**
*
* 1---所在线程:main
*2---所在线程:我的自定义的线程池bqt
*3---所在线程:DefaultDispatcher-worker-2
* 4---所在线程:main
* */
}
fun log(text: Any) = Log.e(TAG,"$text---所在线程:${Thread.currentThread().name}".trimIndent())
}
2.使用withContext()挂起函数切换线程池
在 Andorid 开发中,我们常常在子线程中请求网络获取数据,然后切换到主线程更新UI。官方为我们提供了一个
withContext
顶级函数,在获取数据函数内,调用withContext(Dispatchers.IO)
来创建一个在IO
线程池中运行的块。您放在该块内的任何代码都始终通过IO
调度器执行。
withContext
是一个suspend
挂起函数,它用于在不同的协程上下文(Coroutine Context)中执行代码。它在新的上下文中执行代码块,并返回代码块的结果。
withContext
通常用于在不同的调度器(Dispatcher.IO/MAIN)之间切换协程的执行线程,来保证主线程安全。
使用withContext
函数可以改变当前协程的上下文,而仍然驻留在相同的当前协程中,同时withContext
还携带有一个泛型T
返回值。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
......
}
使用withContext函数标准模版:
通过调度器Dispatchers 让当前协程launch运行在主线程Dispatchers.Main
在协程launch的代码块中,如果当前主线程执行到了 suspend withContext函数这里的时候,该协程launch就被挂起了,并且当前协程launch的主线程执行权被掐断,
当执行suspend withContext 函数的时候,通过调度器Dispatchers 切换到指定的IO线程, 此时执行suspend withContext 函数里面的代码逻辑(网络请求,IO读写操作)
那么, 此时暂时就不再执行该协程launch内的 suspend withContext 函数下面剩余的其他代码逻辑了
当前协程launch被挂起时,那么就执行协程launch的代码块以最外层的其他逻辑代码
当suspend withContext函数执行完成之后,协程launch会自动帮我们从IO线程再切回来到原来主线程,
那么当前协程launc的主线程就会恢复resume执行, 就会继续执行下面的代码:更新UI
class SuspendActivity : ComponentActivity() {
private val mainViewModel :MainViewModel by viewModels<MainViewModel>()
val tv_title:TextView? by lazy{findViewById<TextView>(R.id.tv_title)}
override fun onCreate(savedInstanceState: Bundle?){
mainScope.launch(context=Dispatchers.Main,block={
//通过调度器Dispatchers 让当前协程launch运行在主线程Main
// 在协程launch的代码块中,如果当前主线程执行到了 suspend withContext函数这里的时候,该协程launch就被挂起了,并且当前协程launch的主线程执行权被掐断,
//当执行suspend withContext 函数的时候,通过调度器Dispatchers 切换到指定的IO线程, 此时执行suspend withContext 函数里面的代码逻辑(网络请求,IO读写操作)
val image:String = mainViewModel.suspendingGetImage(9527)
// 那么, 此时暂时就不再执行该协程launch内的 suspend withContext 函数下面剩余的其他代码逻辑了
//此行代码暂时不会执行
//....
//....
// 当suspend withContext函数执行完成之后,协程launch会自动帮我们从IO线程再切回来到原来主线程,
// 那么当前协程launc的主线程就会恢复resume执行, 就会继续执行下面的代码:更新UI
tv_title?.text=image
Log.e(TAG, "当前协程launch的主线程就会恢复resume执行, 就会继续执行下面的代码:更新UI:${tv_title?.text}")
})
// 当前协程launch被挂起时,那么就执行协程launch的代码块以外层的其他逻辑代码
tv_title?.text="123456789"
Log.e(TAG, "当前协程launch被挂起时,那么就执行协程launch的代码块以外层的其他逻辑代码 =${tv_title?.text}")
/** 打印日志执行顺序:
当前协程launch被挂起时,那么就执行协程launch的代码块以外层的其他逻辑代码 =123456789
执行suspend withContext 函数开始:网络请求
10s后,执行suspend withContext 函数结束
当前协程launch的主线程就会恢复resume执行, 就会继续执行下面的代码:更新UI:大头像9527
*/
}
}
class MainViewModel:ViewModel() {
suspend fun suspendingGetImage(id: Int):String = withContext(context=Dispatchers.IO,block={
Log.e(TAG, " 执行suspend withContext 函数开始:网络请求")
delay(10000) //模拟网络请求耗时10s
Log.e(TAG, " 10s后,执行suspend withContext 函数结束")
"大头像$id"
})
}
注意:如果你有多个并列的耗时网络请求或者IO操作需要同时操作的话,
那么你就需要创建多个不同的子协程,每个子协程默认在主线程,
同时在每个子协程里使用suspend withContext函数使用调度器Dispatchers切换到子线程里去执行网络请求或者IO操作
不同在一个(子)协程里同时使用多个suspend withContext函数分别去执行网络请求或者IO操作,
因为当执行到第一个suspend withContext函数时,该(子)协程就被挂起掐断了, 后面的其他的suspend withContext函数是不会执行的。
class SuspendActivity : ComponentActivity() {
private val mainViewModel :MainViewModel by viewModels<MainViewModel>()
val tv_title:TextView? by lazy{findViewById<TextView>(R.id.tv_title)}
val tv_name:TextView? by lazy{findViewById<TextView>(R.id.tv_name)}
override fun onCreate(savedInstanceState: Bundle?){
val launch1 = mainScope.launch(context=Dispatchers.IO,block={
val result1: User = mainViewModel.getUserSuspend(timeMillis=10000,id=9527,name="周星驰")
withContext(context=Dispatchers.Main,block={
tv_title?.text=result1.name
Log.e(TAG, " 10s后,子协程launch1执行suspend withContext 函数:切换到主线程更新UI数据:tv_title?.text=${result1.name}")
})
})
val launch2 = mainScope.launch(context=Dispatchers.IO,block={
val result2: User = mainViewModel.getUserSuspend(timeMillis=5000,id=37,name="刘亮")
withContext(context=Dispatchers.Main,block={
tv_name?.text=result2.name
Log.e(TAG, " 5s后,子协程launch2执行suspend withContext 函数:切换到主线程更新UI数据:tv_name?.text=${result2.name}")
})
})
tv_title?.text="zhouxingchi"
tv_name?.text="liuliang"
/**打印日志输出:
* 5s后,子协程launch2执行suspend withContext 函数:切换到主线程更新UI数据:tv_name?.text=刘亮
*10s后,子协程launch1执行suspend withContext 函数:切换到主线程更新UI数据:tv_title?.text=周星驰
* */
}
}
案例:
创建一个协程launch,在主线程
遇到withContext(Dispatchers.IO)挂起函数,通过调度器Dispatchers切换到IO线程,Lambda表达式中的代码就会在IO线程里做网络请求执行操作,
而它外部的其他的所有代码仍然还是运行在 main 线程执行其他的逻辑操作。
当 IO线程 耗时操作完成, 就会恢复主线程继续执行,完成UI的更新
private fun withContextTest2() {
//创建一个协程 作用域在主线程里
GlobalScope.launch(Dispatchers.Main) {
Log.e(TAG, "当前所处的线程1: ${Thread.currentThread().name} ")
Log.e(TAG, "获取 协程在上下文指定的调度器1:${this.coroutineContext}")
// 通过withContext(Dispatchers.IO)调度(切换)到IO线程上去做网络请求
//协程作用域的主线程被挂起
val result: User = withContext(context=Dispatchers.IO,
block= {//切换到IO线程 网络请求
Log.e(TAG, "当前所处的线程2: ${Thread.currentThread().name} ")
Log.e(TAG, "获取 协程在上下文指定的调度器2:${this.coroutineContext}")
mainViewModel.getUserSuspend()
})
Log.e(TAG, "当前所处的线程3: ${Thread.currentThread().name} ")
Log.e(TAG, "获取 协程在上下文指定的调度器3:${this.coroutineContext}")
// 协程作用域的主线程恢复继续执行,在主线程完成UI的更新
tv_title.text=result.name
}
}
/**
*当前所处的线程1: main
*获取 协程在上下文指定的调度器1:[StandaloneCoroutine{Active}@39672b3, Dispatchers.Main]
*当前所处的线程2: DefaultDispatcher-worker-1
* 获取 协程在上下文指定的调度器2:[DispatchedCoroutine{Active}@7739070, Dispatchers.IO]
*
* 网络耗时操作完成后输出
* 当前所处的线程3: main
* 获取 协程在上下文指定的调度器3:[StandaloneCoroutine{Active}@39672b3, Dispatchers.Main]
* */
private fun withContextTest3() {
//创建一个协程作用域在子线程里
GlobalScope.launch(Dispatchers.IO){
Log.e(TAG, "当前所处的线程1: ${Thread.currentThread().name} ")
Log.e(TAG, "获取 协程在上下文指定的调度器1:${this.coroutineContext}")
//在协程作用域IO线程 网络请求耗时操作 直至返回结果
val result: User = mainViewModel.getUserSuspend()
//将协程作用域子线程切换到主线程 在主线程完成UI的更新 显示返回的网络数据result
withContext(Dispatchers.Main){
Log.e(TAG, "当前所处的线程2: ${Thread.currentThread().name} ")
Log.e(TAG, "获取 协程在上下文指定的调度器2:${this.coroutineContext}")
tv_title.text=result.name
}
}
}
/**
* 当前所处的线程1: DefaultDispatcher-worker-1
获取 协程在上下文指定的调度器1:[StandaloneCoroutine{Active}@f7a3f7d, Dispatchers.IO]
网络耗时操作完成后输出
当前所处的线程2: main
获取 协程在上下文指定的调度器2:[DispatchedCoroutine{Active}@30f9321, Dispatchers.Main]
*/
协程上下文CoroutineContext:
协程的上下文,由不同的元素(CoroutineDispatcher、Job、CoroutineName ,CoroutineExceptionHandler)组成,分别定义协程的不同行为。包含以下元素:
Job - 控制协程的生命周期
CoroutineDispatcher - 将工作分配给合适的线程
CoroutineName - 协程的名字,调试的时候有用
CoroutineExceptionHandler - 处理未捕获的异常
协程上下文的内部实现实际是一个单链表。
CoroutineContext
表示协程上下文,是 Kotlin 协程的一个基本结构单元。协程上下文主要承载着资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。它有很多作用,包括携带参数,拦截协程执行等等。如何运用协程上下文是至关重要的,以此来实现正确的线程行为、生命周期、异常以及调试。
协程上下文的数据结构特征更加显著,与List和Map非常类似。它包含用户定义的一些数据集合,这些数据与协程密切相关。它是一个有索引的 Element
实例集合。每个 element
在这个集合有一个唯一的Key
。
//协程的持久上下文。它是[Element]实例的索引集,这个集合中的每个元素都有一个唯一的[Key]。
public interface CoroutineContext {
//从这个上下文中返回带有给定[key]的元素或null。
public operator fun <E : Element> get(key: Key<E>): E?
//从[initial]值开始累加该上下文的项,并从左到右应用[operation]到当前累加器值和该上下文的每个元素。
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
//返回一个上下文,包含来自这个上下文的元素和来自其他[context]的元素。
public operator fun plus(context: CoroutineContext): CoroutineContext
//返回一个包含来自该上下文的元素的上下文,但不包含指定的[key]元素。
public fun minusKey(key: Key<*>): CoroutineContext
//[CoroutineContext]元素的键。[E]是带有这个键的元素类型。
public interface Key<E : Element>
//[CoroutineContext]的一个元素。协程上下文的一个元素本身就是一个单例上下文。
public interface Element : CoroutineContext {
//这个协程上下文元素的key
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E?
}
}
Element
Element
是CoroutineContext
的内部接口,同时它又实现了CoroutineContext
接口,这么设计的原因是为了保证Element
中一定只能存放的Element
它自己,而不能存放其他类型的数据
Key
CoroutineContext
内还有一个内部接口Key
,同时它又是Element
的一个属性,这个属性很重要,通过Key从协程上下文中获取我们想要的Element
plus
() : 使用 +
运算符来重载这个方法
有个关键字operator
表示这是一个运算符重载的方法,类似List.plus的运算符,可以通过+
号来返回一个包含原始集合和第二个操作数中的元素的结果。CoroutineContext
中是通过plus
来返回一个由原始的Element
集合和通过+
号引入的Element
产生新的Element
集合。
用
operator
修饰plus()
方法后,就可以用+
运算符来重载这个方法
- 比如,集合之间的合并操作:
list3 = list1 + list2
、map3 = map1 + map2
/**
* 返回一个包含来自此上下文和来自其他[context]的元素的上下文。
* 该上下文中与另一个上下文中具有相同键的元素将被删除。
*/
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
// make sure interceptor is always last in the context (and thus is fast to get when present)
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
// 将主线程包装成协程
runBlocking<Unit>{
launch(
// 为 协程上下文 指定 协程调度器 + 协程名称 两个元素
//使用 + 运算符 , 为协程上下文 CoroutineContext 指定
//协程调度器 Dispatchers.Default
// 协程名称 CoroutineName("Hello")
Dispatchers.Default + CoroutineName("Hello")
) {
Log.e(TAG, "当前运行的线程 : ${Thread.currentThread().name}")
}
}
get(key: Key<E>)
get
方法,顾名思义。可以通过 key
来获取一个Element
fold()
fold
方法它和集合中的fold是一样的,用来遍历当前协程上下文中的Element
集合。
minusKey()
minusKey
方法plus
作用相反,它相当于是做减法,是用来取出除key
以外的当前协程上下文其他Element
,返回的就是不包含key
的协程上下文。
Element
协程使用以下几种元素集定义协程的行为,它们均继承自CoroutineContext
:
这些Element
都有需要有一个CoroutineContext.Key
类型的伴生对象key
协程上下文包含的元素Element | 定义协程的行为 |
Job : CoroutineContext.Element | 协程的句柄,对协程的控制和管理生命周期。 |
CoroutineName | 协程的名称,可用于调试。 |
| 调度器,确定协程在指定的线程来执行。 |
: CoroutineContext.Element | 协程异常处理器,处理未捕获的异常。 |
: CoroutineContext.Element |
Job :
协程的句柄
Job
间接实现了 CoroutineContext
接口,所以,Job 本身就是一个 CoroutineContext
。
ob 其实就是协程的句柄
,通过 Job 对象,我们主要可以监测及操控协程。
public interface Job : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<Job> {
//省略...
}
}
public interface CoroutineContext {
public interface Element : CoroutineContext {}
}
CoroutineDispatcher
协程调度器
用于 分发协程任务 , 被调度主体是 线程 , 也就是安排哪个线程执行哪个任务 ;
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(
ContinuationInterceptor,
{ it as? CoroutineDispatcher })
}
CoroutineExceptionHandler
协程异常处理器
CoroutineExceptionHandler
也间接实现了 CoroutineContext
接口,它主要负责处理协程当中的异常。 用于处理协程中 未被捕获的异常 ;
public interface CoroutineExceptionHandler : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
public fun handleException(context: CoroutineContext, exception: Throwable)
}
fun main() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { context: CoroutineContext, throwable ->
println("${context[CoroutineName]?.name} - ${throwable.message}")
println(throwable.stackTraceToString())
}
GlobalScope.launch(CoroutineName("bqt") + exceptionHandler) {
throw Exception("自定义异常")
}.join()
}
ContinuationInterceptor
public interface ContinuationInterceptor : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}
CoroutineName
协程名称
CoroutineName 也间接实现了 CoroutineContext 接口,可用于指定协程的名称
CoroutineName是用户用来指定的协程名称的,用于方便调试和定位问题
//用户指定的协程名称。此名称用于调试模式。
public data class CoroutineName(
//定义协程的名字
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
//CoroutineName实例在协程上下文中的key
public companion object Key : CoroutineContext.Key<CoroutineName>
}
协程内部可以通过coroutineContext
这个全局属性直接获取当前协程的上下文。
GlobalScope.launch(CoroutineName("GlobalScope")) {//指定父协程的名称
this.launch(CoroutineName("CoroutineA")) {//指定子协程名称
val coroutineName = coroutineContext[CoroutineName]//获取子协程名称
print(coroutineName)
}
}
上下文组合
从上面的协程创建的函数中可以看到,协程上下文的参数只有一个,但是怎么传递多个上下文元素呢?CoroutineContext
可以使用 " + " 运算符进行合并。由于CoroutineContext
是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的CoroutineContext
。
注意:如果有重复的元素(key
一致)则会右边的会代替左边的元素。
private fun CoroutineContextTest() {
GlobalScope.launch {
//通过+号运算添加多个上下文元素
var context = CoroutineName("上下文1") + Dispatchers.Main
Log.e(TAG, "当前协程的上下文元素=$context")
// 当前协程的上下文元素=[CoroutineName(上下文1), Dispatchers.Main]
context += Dispatchers.IO + CoroutineName("上下文2")//添加重复Dispatchers元素,Dispatchers.IO 会替换 ispatchers.Main
Log.e(TAG, "当前协程的上下文元素=$context")
// 当前协程的上下文元素=[CoroutineName(上下文2), Dispatchers.IO]
context = context.minusKey(context[CoroutineName]!!.key) //移除CoroutineName元素
Log.e(TAG, "当前协程的上下文元素=$context")
// 当前协程的上下文元素=Dispatchers.IO
}
}
协程上下文元素的继承关系
协程上下文元素的继承 : 在 线程 / 协程 中 可以 创建协程 , 创建协程时 , 需要设置 协程上下文 CoroutineContext , 在协程上下文 中 不同元素 有不同的 继承形式 ;
- 协程任务 Job , 是全新的 ;
- 协程调度器 CoroutineDispatcher | 协程名称 CoroutineName | 协程异常处理器 CoroutineExceptionHandler 三个元素会从 协程上下文 CoroutineContext 父类 继承 ;
案例1:在 协程 A 中 创建 协程 B , 则协程 A 的 协程上下文 CoroutineContext 就是协程 B 的 协程上下文 CoroutineContext 的 父类 ;
private fun job1job2job3() {
Log.e(TAG, "当前所处的线程1: ${Thread.currentThread().name} ") //当前所处的线程1: main
runBlocking {
Log.e(TAG, "当前所处的线程2: ${Thread.currentThread().name} ") // 当前所处的线程2: main
Log.e(TAG, "获取 协程在上下文指定的调度器2:${this.coroutineContext}") //获取 协程在上下文指定的调度器2:[BlockingCoroutine{Active}@4d2bdf4, BlockingEventLoop@253691d]
// 协程1 coroutineScope
val coroutineScope = CoroutineScope(Job() + Dispatchers.Default + CoroutineName("协程1(coroutineScope)"))
// 协程2 job2
// todo 子协程job2的协程上下文集成父类协程 :协程1 coroutineScope的
val job2 = coroutineScope.launch(){
Log.e(TAG, "获取 协程在上下文指定的调度器3:${this.coroutineContext}")//获取 协程在上下文指定的调度器3:[CoroutineName(协程1(coroutineScope)), StandaloneCoroutine{Active}@f8c792, Dispatchers.Default]
Log.e(TAG, "当前所处的线程3: ${Thread.currentThread().name} ") //当前所处的线程3: DefaultDispatcher-worker-1
// 协程 3 job3
val job3 = this.launch {
Log.e(TAG, "获取 协程在上下文指定的调度器4:${this.coroutineContext}")//获取 协程在上下文指定的调度器4:[CoroutineName(协程1(coroutineScope)), StandaloneCoroutine{Active}@3a7a563, Dispatchers.Default]
Log.e(TAG, "当前所处的线程4: ${Thread.currentThread().name} ") //当前所处的线程4: DefaultDispatcher-worker-2
}
// 等待 job3 任务执行完毕
job3.join()
}
// 等待 job2 执行完毕
job2.join()
}
}
/**
* 协程 1 是 协程 2 的父类协程
* 协程 2 是 协程 3 的父类协程
*
*
* */
协程上下文元素的几种指定形式 ( 默认 | 继承 | 自定义指定 )
协程任务 的 协程上下文元素 由以下几种形式指定 :
① 默认的 协程上下文 CoroutineContext :
下面代码中 launch 构建的协程就是默认参数 ;
- 默认 协程调度器 CoroutineDispatcher : Dispatchers.Default ;
- 默认 协程名称 CoroutineName :
" coroutine "
;
private fun defaultCoroutineContext() {
runBlocking {
launch {
Log.e(TAG, "当前所处的线程: ${Thread.currentThread().name} ")
Log.e(TAG, "获取 协程在上下文指定的调度器:${this.coroutineContext}")
}
}
}
/**
* 当前所处的线程: main
* 获取 协程在上下文指定的调度器:[StandaloneCoroutine{Active}@48c841f, BlockingEventLoop@f0fe36c]
* */
② 继承自父类的 协程上下文 CoroutineContext :
继承自 父协程 或 CoroutineScope 的 协程上下文 ; 参考 " 以上 协程上下文元素的继承关系 " 中的示例 ;
③ 自定义的 协程上下文 CoroutineContext 元素参数 :
在 协程构建器 中指定的 协程上下文参数 优先级最高 , 可以 覆盖 默认值 和 继承自父类的 协程上下文元素 , 如下代码示例 ;
案例:
private fun customCoroutineContext() {
// 将主线程包装成协程
runBlocking {
// 协程异常处理器
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e(TAG, "处理协程异常 : ${throwable}")
}
// 创建协程作用域
// 协程 1
val coroutineScope = CoroutineScope(
Job() + // 协程任务
Dispatchers.Main + // 协程调度器
CoroutineName("协程 1") + // 协程名称
coroutineExceptionHandler // 协程异常处理器
)
Log.e(TAG, "当前所处的线程: ${Thread.currentThread().name} ") //当前所处的线程: main
Log.e(TAG, "获取 协程在上下文:${this.coroutineContext}") //获取 协程在上下文指定的调度器:[BlockingCoroutine{Active}@48c841f, BlockingEventLoop@f0fe36c]
// 协程 2
// 在 CoroutineScope(协程 1) 中创建 子协程(协程 2) ,
// 子协程(协程 2)上下文都继承自 coroutineScope (协程 1)的协程上下文
val job2 = coroutineScope.launch (Dispatchers.IO){
// 通过线程查看协程调度器
Log.e(TAG, "当前所处的线程2: ${Thread.currentThread().name} ") //当前所处的线程2: DefaultDispatcher-worker-2
Log.e(TAG, "获取 协程在上下文2:${this.coroutineContext}")
//获取 协程在上下文2:[CoroutineName(协程 1), com.my.runalone_coroutinedemo.MainActivity$customCoroutineContext
// $1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@296ed35, StandaloneCoroutine{Active}@28755ca, Dispatchers.IO]
// 协程 3
// 在 job2 协程中创建 子协程 (协程 3),
// 子协程 (协程 3)上下文都继承自 job2 的协程上下文
val job3 = launch() {
// 通过线程查看协程调度器 , 该协程的 协程调度器 是 Dispatchers.IO
Log.e(TAG, "当前所处的线程3: ${Thread.currentThread().name} ") // 当前所处的线程3: DefaultDispatcher-worker-3
Log.e(TAG, "获取 协程在上下文3:${this.coroutineContext}")
// 获取 协程在上下文3:[CoroutineName(协程 1), com.my.runalone_coroutinedemo.MainActivity$customCoroutineContext$1
// $invokeSuspend$$inlined$CoroutineExceptionHandler$1@296ed35, StandaloneCoroutine{Active}@3ff433b, Dispatchers.IO]
}
// 等待 job3 任务执行完毕
job3.join()
}
// 等待 job2 执行完毕
job2.join()
}
}
/**
协程 1 是 协程 2 的父类协程
协程 2 是 协程 3 的父类协程
*/
协程启动模式
CoroutineStart.DEFAULT 启动模式
协程是以非懒加载
的方式创建的,那么它的初始状态是 Active,
协程马上开始调度执行 。
默认启动模式,饿汉启动模式, 协程创建后,立即开始调度执行(没有cancel);
如果在 执行前或执行时 取消协程 , 则进入 取消响应 状态 ;
如果在主线程中执行协程 , 协程挂起后 , 主线程继续执行其它任务, 如刷新 UI 等 , 主线程不会阻塞 , 挂起函数会在子线程中执行 ;
一般会将耗时操作放在 协程的挂起函数 中执行 ;
案例1:立即调度,立即执行,协程没有取消
/**
* 立即调度,立即执行,协程没有取消
* */
private fun DEFAULTTest1() {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// 指定协程的启动模式为 CoroutineStart.DEFAULT
// 默认的 协程启动模式 , 协程创建后 , 马上开始调度执行 ,
runBlocking {
val job = launch(start = CoroutineStart.DEFAULT) {
delay(2000)
Log.e(TAG, "协程开始执行1")
delay(2000)
Log.e(TAG, "协程执行完毕")
}
delay(1000)
Log.e(TAG, "协程开始执行2")
}
/**
* 协程开始执行2
* 协程开始执行1
* 协程执行完毕
*
* */
}
案例2: 在执行过程中 , 协程被取消 ,协程执行任务也会被取消
/**
* 在执行过程中 , 协程被取消 ,执行也会被取消
* */
private fun DEFAULTTest2() {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// 指定协程的启动模式为 CoroutineStart.DEFAULT
// 默认的 协程启动模式 , 协程创建后 , 马上开始调度执行 ,
runBlocking {
// launch 启动协程 , 该协程运行在主线程中
val job = launch(start = CoroutineStart.DEFAULT) {
Log.e(TAG, "协程开始执行1") //此时协程还未取消 协程任务在执行中,此日志 会打印
delay(2000)
Log.e(TAG, "协程执行完毕")//延迟2s后, 协程已经被取消了 协程执行任务也被取消不再执行了 此日志不会打印
}
// 延时 1 秒, 立刻执行 job1.cancel()
delay(1000)
// job协程被取消,那么协程的执行任务就会被取消
job.cancel()
Log.e(TAG, "协程开始执行2")
}
/**
* 输出日志:
* 协程开始执行1
* 协程开始执行2
* */
}
/**
* 在执行过程中 , 协程被取消 ,协程执行任务也会被取消
* */
private fun DEFAULTTest3() {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
runBlocking {
val job = launch(start = CoroutineStart.DEFAULT) {
delay(2000)
Log.e(TAG, "协程开始执行1") //延迟1s 协程就被取消了 协程任务就会取消 此日志不会打印
delay(2000)
Log.e(TAG, "协程执行完毕")//延迟1s 协程就被取消了 协程任务就会取消 此日志不会打印
}
delay(1000)
// 延迟1s, job协程被取消,那么协程任务就会取消,导致协程任务里的逻辑并没有执行
job.cancel()
Log.e(TAG, "协程开始执行2")
}
/**
* 输出日志:
协程开始执行2
* */
}
案例3: 协程立即调度,但是在协程执行任务前,协程就被取消了. 协程体没有任何执行输出。
/**
* 在协程执行任务前,协程就被取消了. 协程体没有任何执行输出。
* */
private fun DEFAULTTest4(){
runBlocking {
val job = launch(start = CoroutineStart.DEFAULT) {
Log.e(TAG,"start")
delay(5000)
Log.e(TAG,"done")
}
job.cancel()
}
}
CoroutineStart.LAZY
启动模式
如果 Job 是以LAZY懒加载
的方式创建的,那么它的初始状态是 New。
当协程创建后 ,并不会有任何调度行为,协程不会立即执行.直到我们需要它执行的时候才会产生调度。
也就是说只有我们主动的调用Job
的start
、join
或者await
等函数时才会开始调度执行,当前生命周期状态才变成Active状态。
在下面的代码中 , val job = async (start = CoroutineStart.LAZY) 只是定义协程 , 并不会马上执行 , 在执行 job.start() 或 job.await() 代码时 , 才开始调度执行协程 , 如果在这之前调用 job.cancel() 取消协程 , 则协程直接取消 ;
案例1:
private fun LAZYTest1() {
runBlocking {
val job1 = async(start = CoroutineStart.LAZY) {
Log.e(TAG, "协程执行了1")
delay(2000)
Log.e(TAG, "协程执行了2")
"Hello" // 返回一个字符串
}
Log.e(TAG, "协程调度了1 ")
// 执行下面两个方法中的任意一个方法 ,
// 启动执行协程
job1.start()
// 获取协程返回值
job1.await()
Log.e(TAG, "协程调度了2 ")
}
/**
* 协程调度了1
* 协程执行了1
* 协程执行了2
* 协程调度了2
*
* */
}
案例2:
private fun LAZYTest2() {
runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
Log.e(TAG, "协程执行了1")
delay(5000)
Log.e(TAG, "协程执行了2")
}
job.start()
}
}
/**
* 协程执行了1
* 协程执行了2
* */
案例3:
如果在 调度之前就取消协程 , 协程将直接进入异常结束状态。
private fun LAZYTest3() {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// 指定协程的启动模式为 CoroutineStart.LAZY
// 协程创建后 , 不会马上开始调度执行 ,
// 只有 主动调用协程的 start , join , await 方法 时 , 才开始调度执行协程 ,
runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
Log.e(TAG, "协程执行了1")
delay(5000)
Log.e(TAG, "协程执行了2")
}
// 在 调度之前就取消协程 , 协程将直接进入异常结束状态。
job.cancel()
}
}
CoroutineStart.ATOMIC
启动模式
协程是以非懒加载
的方式创建的,那么它的初始状态是 Active,
协程马上开始调度执行 。
通过ATOMIC
模式启动的协程执行到第一个挂起点(suspend函数)之前, 如果取消协程 , 则不进行响应取消操作 ,协程依然执行。
ATOMIC
一定要涉及到协程挂起后,cancel
取消操作的时候才有意义。
案例1:
private fun ATOMICTest() {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// 指定协程的启动模式为 CoroutineStart.ATOMIC
// 协程创建后 , 马上开始调度执行 ,
// 协程执行到 第一个挂起点delay(3000) 之前 , 如果取消协程 , 则不进行响应取消操作,协程依然执行。
runBlocking {
val job1 = launch(start = CoroutineStart.ATOMIC) {
// 如果立刻调用了 job1.cancel(),下面依旧会执行 打印 ,直到遇到挂起函数 delay(10000)后,才会被终止执行
Log.e(TAG, "协程执行了1") // 依旧会执行 打印
delay(3000)
Log.e(TAG, "协程执行了2") //被终止执行 不打印
}
Log.e(TAG, "协程调度了1 ")
// 关闭协程
job1.cancel()
Log.e(TAG, "协程调度了2 ")
}
/**
协程执行后 , 遇到的 第一个挂起函数是 delay(2000) 函数 , 该 挂起函数之前的代码执行过程中 , 如果取消协程 , 则该 协程不会取消 ,
直到执行到 第一个挂起函数是 delay(2000) 函数 时 , 协程才会取消 ;
输入日志:
协程调度了1
协程调度了2
协程执行了1
*/
}
案例2:
private fun ATOMICTest2() {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// 指定协程的启动模式为 CoroutineStart.ATOMIC
// 协程创建后 , 马上开始调度执行 ,
// 协程执行到 第一个挂起点delay(3000) 之前 , 如果取消协程 , 则不进行响应取消操作,协程依然执行。
runBlocking {
val job1 = launch(start = CoroutineStart.ATOMIC) {
// 如果立刻调用了 job1.cancel(),下面依旧会执行 打印 ,直到遇到挂起函数 delay(10000)后,才会被终止执行
Log.e(TAG, "协程执行了1") // 依旧会执行 打印
delay(5000)
Log.e(TAG, "协程执行了2") //被终止执行 不打印
}
delay(1000)
Log.e(TAG, "协程调度了1 ")
// 关闭协程
job1.cancel()
Log.e(TAG, "协程调度了2 ")
}
/**
协程执行了1
协程调度了1
协程调度了2
*/
}
CoroutineStart.UNDISPATCHED
启动模式
协程是以非懒加载
的方式创建的,那么它的初始状态是 Active,
协程马上开始调度执行 。
协程创建后 , 立即在当前的 函数调用栈(主线程) 执行协程任务 , 直到遇到第一个挂起函数 , 才在子线程中执行挂起函数 ;
如果在主线程中启动协程 , 则该模式的协程就会直接在主线程中执行 ;
如果在子线程中启动协程 , 则该模式的协程就会直接在子线程中执行 ;
案例1:
private fun UNDISPATCHEDTest2() {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// 指定协程的启动模式为 CoroutineStart.UNDISPATCHED
// 协程创建后 , 立即在当前的 函数调用栈 执行协程任务 ,
// 直到遇到第一个挂起函数 , 才在子线程中执行挂起函数 ;
runBlocking {
val job = async ( context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED ) {
// Dispatchers.IO 调度器是将协程调度到子线程执行
// 但是如果 协程启动模式为 UNDISPATCHED , 则立刻在当前的主线程中执行协程
// 协程创建后 , 立即在当前的 函数调用栈 执行协程任务 , 因此会打印主线程
Log.e(TAG, "协程执行了1 ${Thread.currentThread().name} ") // 协程执行了1 main //还在主线程执行
//直到遇到 delay(5000) 挂起 函数时,协程才会切换到 Dispatchers.IO 调度器中去执行,在子线程执行该挂起函数
// 挂起函数都是耗时任务
delay(5000)
Log.e(TAG, "协程执行了2 ${Thread.currentThread().name} ") //协程执行了2 DefaultDispatcher-worker-1 // 切换到了子线程执行
"Hello" // 返回一个字符串
}
job.join()
}
}
/**
* 协程执行了1 main
* 协程执行了2 DefaultDispatcher-worker-2
* */
CoroutineStart 中定义的协程启动模式原型
机翻文档 , 仅供参考 ;
package kotlinx.coroutines
import kotlinx.coroutines.CoroutineStart.*
/**
* 定义协同程序构建器的开始选项。
* 它用于[launch][CoroutineScope的' start '参数中。发射],[异步][CoroutineScope。以及其他协程构建器函数。
*
* 协程启动选项的汇总如下:
* * [DEFAULT]——根据上下文立即安排协程执行;
* * [LAZY]—只在需要时才启动协程;
* * [ATOMIC]——原子地(以不可取消的方式)根据上下文安排协程执行;
* * [UNDISPATCH]——立即执行协程,直到它在当前线程中的第一个挂起点_。
*/
public enum class CoroutineStart {
/**
* Default——根据上下文立即安排协程执行。
*
* 如果协程上下文的[CoroutineDispatcher]从[CoroutineDispatcher. isdispatchneeded]返回' true '
* 像大多数调度程序那样运行,那么协程代码稍后被调度执行,而代码则被调度执行
* 调用的协程构建器继续执行。
*
* 注意[Dispatchers.]总是从它的[CoroutineDispatcher.isDispatchNeeded]返回' false '
* 函数,因此启动与[Dispatchers.]的协程。使用[DEFAULT]与使用[undispatch]相同。
*
* 如果协程[Job]在它甚至有机会开始执行之前被取消,那么它将不会启动它的
* 执行,但将以异常完成。
*
* 协程在挂起点的可取消性取决于的具体实现细节
* 暂停功能。使用[suspendCancellableCoroutine]实现可取消的挂起函数。
*/
DEFAULT,
/**
* 只有在需要时才会惰性地启动协程。
*
* 有关详细信息,请参阅相应协程构建器的文档
* (如[发射][CoroutineScope。和[async][CoroutineScope.async])。
*
* 如果协程[Job]在它甚至有机会开始执行之前被取消,那么它将不会启动它的
* 执行,但将以异常完成。
*/
LAZY,
/**
* 原子地(即,以一种不可取消的方式)根据上下文安排协程的执行。
* 这类似于[DEFAULT],但是协程在开始执行之前不能被取消。
*
* 协程在挂起点上的可取消性取决于的具体实现细节
* suspend功能如[DEFAULT]。
*/
@ExperimentalCoroutinesApi // Since 1.0.0, no ETA on stability
ATOMIC,
/**
* 立即执行协程,直到它在当前线程中的第一个挂起点_
* 正在使用[dispatchers . unrestricted]启动协程。但是,当从挂起恢复协程时
* 它根据上下文中的[CoroutineDispatcher]进行分派。
*
* 这与[ATOMIC]在某种意义上类似,协程开始执行,即使它已经被取消,
* 但不同的是,它在同一个线程中开始执行。
*
* 协程在挂起点上的可取消性取决于的具体实现细节
* suspend功能如[DEFAULT]。
*
* 无限制事件循环
*
* 与调度程序。和[MainCoroutineDispatcher。],嵌套的未分派协程不会形成
* 在无限制嵌套的情况下防止潜在堆栈溢出的事件循环。
*/
UNDISPATCHED;
/**
* 当[LAZY]时返回' true '。
*
* @suppress **这是一个内部API,不应该从通用代码中使用
*/
@InternalCoroutinesApi
public val isLazy: Boolean get() = this === LAZY
}
关键字susend「 挂起」的深入理解:
见另一篇文章:
补篇协程:关键字suspend「挂起」的深入理解-CSDN博客
协程的【挂起】和【恢复】
❶广义的协程,可以理解为互相协作的程序
,也就是 Cooperative-routine
❷协程框架,封装了 Java 的线程,对开发者暴露了协程的 API
❸程序当中运行的协程
,可以理解为轻量的线程
❹一个线程当中,可以运行成千上万个协程
❺协程,也可以理解为运行在线程当中的非阻塞的 Task
❻协程,通过挂起和恢复的能力,实现了非阻塞
❼协程不会与特定的线程绑定,它可以在不同的线程之间灵活切换,这其实也是通过挂起和恢复
来实现的
1.函数 最基本的操作 是 :
- 调用 call : 通过 函数名或函数地址 调用函数 ;
- 返回 return : 函数执行完毕后 , 继续执行函数调用的下一行代码 ;
2.协程 在 调用 call 和 返回 return 基础上 , 又新增了两种 状态 :
- 挂起 Suspend : 暂停当前执行的协程 , 保存挂起点的局部变量 , 然后执行异步任务 , 后面的代码会等到异步任务执行完毕后 , 恢复 Resume 挂起状态后再执行后续代码 ;
- 恢复 Resume : 暂停的协程 继续执行 ;
如果 没有挂起Suspend 操作 , 在子线程中执行异步任务时 , 会马上执行后续的代码 (不会等待异步任务), 只是相当于 普通的多线程操作并发执行(同时各执行各的,这样就没有意义了) ;
协程的作用就是 可以 顺序地执行 异步任务 和 主线程任务 , 其执行顺序按照代码顺序执行 ;
(执行异步耗时任务时,主协程被挂起(后面的代码不能执行),异步耗时任务执行完毕后,恢复主协程,执行后面的代码)
4.案例:
class MainViewModel:ViewModel() {
suspend fun getUserSuspend(): User {
//在协程中使用 delay 函数 , 挂起 10 秒时间 , 然后 10 秒后更新 UI ;
delay(10000)
return User(33,"方明飞")
}
}
private val mainViewModel = MainViewModel()
val tv_title: TextView by lazy<TextView>{
findViewById<TextView>(R.id.tv_title)
}
private fun suspendTest() {
// 在协程中 GlobalScope.launch(Dispatcher.Main){} 中 , 可以直接调用挂起函数test() ;
GlobalScope.launch (context=Dispatchers.Main, block = {
test()
})
}
private suspend fun test() {
asynTask()
updateMain()
}
var result: User?=null
private suspend fun asynTask() {
// 子线程中执行异步任务
result = mainViewModel.getUserSuspend()
}
private fun updateMain() {
// 主线程更新 UI
tv_title.text= result?.id.toString() // tv_title.text=33
}
分析上述 挂起 suspend 函数 Test() 的 调用流程 :
1.执行
suspend fun Test()
函数时 , 该函数会放入 应用主线程 的 栈帧 中 ,( 此时栈帧内容 : 栈底 | Test 函数 )
2.继续执行内部的
suspend fun asynTask()
函数时 , 该函数也是挂起函数 , 先进行 挂起 suspend 操作 ,( 此时栈帧内容 : 栈底 |Test 函数 | asynTask 函数 )
3.然后执行异步任务
asynTask()
, 异步任务执行完毕后 , 恢复 resumesuspend fun asynTask()
函数 , 该函数又回到了 主线程 栈帧 中 ,asynTask()
执行完毕后 , 该函数从 栈帧 中移除 ;( 此时栈帧内容 : 栈底 |Test 函数 )
4.栈帧中恢复
suspend fun Test()
函数中 , 继续执行函数的后半部分updateMain()
, 执行主线程更新 UI 内容 ;( 此时栈帧内容 : 栈底 |Test 函数 | updateMain 函数 )
5.自定义 suspend 函数
什么时候需要自定义 suspend 函数
如果你的某个函数比较耗时,也就是要等的操作,那就把它写成
suspend
函数。这就是原则。耗时操作一般分为两类:I/O 操作和 CPU 计算工作。比如文件的读写、网络交互、图片的模糊处理,都是耗时的,通通可以把它们写进
suspend
函数里。另外这个「耗时」还有一种特殊情况,就是这件事本身做起来并不慢,但它需要等待,比如 5 秒钟之后再做这个操作。这种也是
suspend
函数的应用场景。
具体该怎么写自定义 suspend 函数
给自定义函数加上
suspend
关键字,然后使用官方的suspendwithContext函数
把我的自定义suspend函数的内容给包住就可以了。提到用
withContext
是因为它在挂起函数里功能最简单直接:把线程自动切走和切回。当然并不是只有
withContext
这一个函数来辅助我们实现自定义的suspend
函数,比如还有一个挂起函数叫delay
,它的作用是等待一段时间后再继续往下执行代码。
(1)关键字suspend修饰自定义普通函数
class MainActivity2 : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
MainScope().launch(block={
Log.e(TAG, "当前所处的线程1: ${Thread.currentThread().name} ") //
Log.e(TAG, "获取 协程在上下文指定的调度器1:${this.coroutineContext}")
func31()
})
/**当前所处的线程1: main
* 获取 协程在上下文指定的调度器1:[StandaloneCoroutine{Active}@63fda07, Dispatchers.Main]
* func31()是一个无参数、无返回的普通挂起函数
*
* */
}
// 是一个无参数、无返回的普通挂起函数(只能在协程里调用 不能切换协程里的线程)
private suspend fun func31():Unit{
Log.e(TAG,"func31()是一个无参数、无返回的普通挂起函数")
}
}
(2)关键字suspend修饰自定义函数类型
特殊的函数类型(挂起函数) | 额外的接收者类型(接收者对象) | 参数类型列表 | 返回值类型 | 备注 |
suspend () -> Unit | 无 | 无 | 空 | 代表无参数、无返回的挂起函数 |
class MainActivity2 : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
MainScope().launch(block={
Log.e(TAG, "当前所处的线程2: ${Thread.currentThread().name} ") //
Log.e(TAG, "获取 协程在上下文指定的调度器2:${this.coroutineContext}")
f32.invoke()
//或者
// f32()
})
/** 当前所处的线程2: main
* 获取 协程在上下文指定的调度器2:[StandaloneCoroutine{Active}@63fda07, Dispatchers.Main]
*
* func32()是一个 ()->Unit函数类型 的挂起函数
* */
}
//是一个 ()->Unit函数类型 的挂起函数
private suspend fun func32():Unit{
Log.e(TAG,"func32()是一个 ()->Unit函数类型 的挂起函数")
}
val f32:suspend ()->Unit = ::func32 //变量f32是一个带 suspend 的 ()->Unit函数类型引用 ::func3
}
(3)关键字suspend修饰自定义带接收者的函数类型
特殊的函数类型(挂起函数) | 额外的接收者类型(接收者对象) | 参数类型列表 | 返回值类型 | 备注 |
suspend A.(B) -> C | A | B | C | |
suspend A.() -> C | A | C | 代表这个函数是 A 类的成员方法或是扩展方法 | |
class MainActivity2 : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
// 是一个 带Int.()->Unit接收者类型的挂起函数
suspend fun Int.func4():Unit {
Log.e(TAG," 输出接受者对象this=${this}")
Log.e(TAG," func4() 是一个 带Int.()->Unit接收者类型的挂起函数")
}
val f4:suspend Int.()->Unit = Int::func4 // 变量f4是一个 带suspend 带Int.()->Unit接收者类型的函数引用
MainScope().launch(block={
f4.invoke(101)
})
/**
输出接受者对象this=101
func4() 是一个 带Int.()->Unit接收者类型的挂起函数
*/
}
}
(4)挂起函数可以访问协程上下文 自定义 suspend 函数 也可以访问协程上下文,非挂起函数不可以。
import kotlinx.coroutines.*
import kotlin.coroutines.coroutineContext // 注意不要导错包了
class SuspendActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?){
//挂起函数可以访问协程上下文 自定义 suspend 函数 也可以访问协程上下文
runBlocking(context=CoroutineName("myCustom")) {
myCustomSuspend("输出我的自定义suspend函数的上下文信息 ")
}
/**
* 输出我的自定义suspend函数的上下文信息 ---myCustom---[CoroutineName(myCustom), BlockingCoroutine{Active}@27ea7e0, BlockingEventLoop@e53ea99]
* */
}
//import kotlin.coroutines.coroutineContext // 注意不要导错包了
private suspend fun myCustomSuspend(text: Any) {
Log.e(TAG,"$text---${coroutineContext[CoroutineName]?.name}---$coroutineContext")
}
}
6.官方Kotlin 协程框架自带的 suspend
函数
最典型的使用最多的就是官方的 suspend withContext挂起函数:把线程自动切走和切回。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
还有一个使用最多的就是官方的 suspend delay
挂起函数:
会延迟协程执行的时间,不会阻塞当前线程,当前线程可以做其他的逻辑,
延迟结束后继续执行协程。自带 isActive 检查机制。
public suspend fun delay(timeMillis: Long) {}
协程是如何通过挂起和恢复
来实现任务非阻塞的呢?
答案是:非阻塞就是
挂起和恢复(抓手、挂钩、hook)。
默认线程的阻塞表现
站在 CPU 的角度上看,对于执行在普通线程
中的程序来说,它会以类似这样的方式执行:
这时候,当某个任务发生了阻塞
行为的时候,比如 sleep,当前执行的 Task 就会阻塞
后面所有任务的执行:
那么,协程是如何通过挂起和恢复
来实现非阻塞的呢?
大部分语言中都存在一个类似调度中心
的东西,它用来实现对 Task 任务的执行和调度
协程除了拥有调度中心
以外,对于每个协程的 Task,还会多出一个类似 抓手、挂钩、hook
的东西,通过这个东西,我们可以方便对它进行挂起和恢复
协程任务的总体执行流程,大致会像下图描述的这样:
线程的 sleep 之所以是阻塞式的,是因为它会阻挡后续 Task 的执行
协程之所以是非阻塞式的,是因为协程支持挂起和恢复
,当某个 子协程的Task任务 由于某种原因被挂起
后,后续的其他的子协程的 Task 并不会因此被阻塞,继续执行
协程挂起与线程阻塞的关系
协程挂起:
协程挂起某个任务,不会阻塞线程。挂起是协程中的概念 , 只能在协程中使用 ;
协程 挂起 操作 : 在协程中使用 suspend delay() 函数 , 挂起 20 秒时间 , 然后 20 秒后更新 UI ; delay 函数是 挂起 suspend 函数 ;
协程 挂起 操作 不会出现 阻塞 UI 刷新的情况 , 挂起的 10/20 秒不影响 UI 刷新显示 ;协程中有挂起操作 , 会将挂起点的状态保存 , 同时协程停止执行 , 等待挂起函数执行完毕后 , 协程继续执行 ; 相当于阻塞的是协程 , 不会阻塞主线程 ;
线程阻塞:
线程阻塞是线程中的概念 , 可以在主线程和子线程中使用 ;
主线程 阻塞 操作 : 在主线程 中使用 Thread.sleep 函数 , 阻塞 20 秒时间 , 然后 20 秒后更新 UI ;
但是如果将主线程阻塞 , UI 不再刷新 , 会出现 ANR 崩溃异常 ;图形化 GUI 系统中 , 一般都在主线程中更新 UI , 主线程中都有一个无限循环 , 不断刷新界面 , 如果在主线程中执行了耗时操作 , 就会影响到界面的刷新 , 出现漏帧 , ANR 崩溃异常 ;
协程与线程关系总结:
业界一直有个说法:Kotlin 协程其实就是一个封装的线程框架,其本质是对线程池的进一步封装。
⑴广义的协程,可以理解为互相协作的程序
,也就是 Cooperative-routine
⑵协程框架,封装了 Java 的线程,对开发者暴露了协程的 API
⑶程序当中运行的协程
,可以理解为轻量的线程
⑷一个线程当中,可以运行成千上万个协程
⑸协程,也可以理解为运行在线程当中的非阻塞的 Task
⑹ 协程不会和线程绑定: 协程虽然运行在线程之上,但协程并不会和某个线程绑定,协程是可以在不同的线程之间灵活切换,这其实也是通过挂起和恢复
来实现的
⑺协程跟线程的关系,有点像线程与进程的关系,因为协程不可能脱离线程运行
⑻协程对比线程还有一个特点,协程通过挂起和恢复的能力,实现了非阻塞
(Non Blocking),而线程则往往是阻塞式的。
❶Kotlin 协程的非阻塞
只是语言层面
的
❷当调用 JVM 层面的 Thread.sleep() 的时候,它仍然会变成阻塞式
的
❸在协程中应该尽量避免出现阻塞式
的行为,即尽量使用 delay
,而不是 sleep
案例1:线程中可以运行多个协程
private fun coroutines_thread_test2() {
runBlocking {
repeat(1000_000_000) { //创建启动 10 亿个子协程
launch {
delay(1000000L)
}
}
}
}
案例2:协程是非常轻量的
在下面的代码中,我们尝试启动 10 亿个线程
,这样的代码,在大部分的机器上运行时,都会因为内存不足
等原因而异常退出。如果改用协程
来实现ok没问题。协程是非常轻量
的,所以代码不会因为内存不足而异常退出。
private fun coroutines_thread_test2() {
runBlocking {
repeat(1000_000_000) { //创建启动 10 亿个子协程
launch {
delay(1000000L)
}
}
}
}
案例3:协程不会和线程绑定
协程虽然运行在线程之上,但协程并不会和某个线程绑定,协程是可以在不同的线程之间切换的。
private fun coroutines_thread_test3() {
runBlocking(Dispatchers.IO) {
repeat(3){ //创建启动 3个子协程
launch {
repeat(3){
Log.e(TAG, "当前所处的线程: ${Thread.currentThread().name} ")
delay(100L)
}
}
}
}
}
/**
* coroutine#2 的三次执行,每一次都在不同的线程上。
* 当前所处的线程: DefaultDispatcher-worker-3
* 当前所处的线程: DefaultDispatcher-worker-1
* 当前所处的线程: DefaultDispatcher-worker-5
* 当前所处的线程: DefaultDispatcher-worker-5
* 当前所处的线程: DefaultDispatcher-worker-3
* 当前所处的线程: DefaultDispatcher-worker-4
* 当前所处的线程: DefaultDispatcher-worker-4
* 当前所处的线程: DefaultDispatcher-worker-3
* 当前所处的线程: DefaultDispatcher-worker-2
*
* */
案例4:阻塞案例:线程 + sleep
由于线程的 sleep() 方法是阻塞式的,所以程序的执行流程是线性的。也就是说,
Print-1
会先连续输出三次,然后Print-2
会连续输出三次。即使Print-2
休眠的时间更短。
private fun thread_sleep_test() {
repeat(3) {
Thread.sleep(2000L)
Log.e(TAG, "Print-1: ${Thread.currentThread().name}")
}
repeat(3) {
Thread.sleep(90L)
Log.e(TAG, "Print-2: ${Thread.currentThread().name}")
}
}
/**
* Print-1: main
* Print-1: main
* Print-1: main
* Print-2: main
* Print-2: main
* Print-2: main
*
* */
案例5:非阻塞案例:协程 + delay()【delay 是非阻塞的】
可以看到,这2个子协程是交替打印输出的, 这两个子协程是并行的。
同时,由于协程的 delay() 方法是非阻塞的,所以,即使一个子协程会先执行 delay(100L),但它也并不会阻塞另一个子协程delay(90L) 的运行。
private fun coroutines_delay_test() {
runBlocking {
// 创建一个子协程
launch {
repeat(3) { // 重复打印三次
// delay(10L) // delay-1
delay(100L) //todo delay-1
// delay(100L) // delay-1
Log.e(TAG, "Print-1: ${Thread.currentThread().name}")
}
}
// 创建一个子协程
launch {
repeat(3) { // 重复打印三次
//delay(100L)// delay-2
delay(90L) //todo delay-2
// delay(10L) // delay-2
Log.e(TAG, "Print-2: ${Thread.currentThread().name}")
}
}
}
}
/**
* 如果 delay-1 =10L < delay-2 = 100L,Print-1 肯定全部在 Print-2 之前输出
* Print-1: main
* Print-1: main
* Print-1: main
* Print-2: main
* Print-2: main
* Print-2: main
* 如果delay-1 =100L > delay-2 = 90L, Print-2 和 Print-1 可能交替输出
* Print-2: main
* Print-1: main
* Print-2: main
* Print-1: main
* Print-2: main
* Print-1: main
* 如果delay-1 =100L >> delay-2 = 10L,Print-2 可能全部在 Print-1 之前输出
* Print-2: main
* Print-2: main
* Print-2: main
* Print-1: main
* Print-1: main
* Print-1: main
*
* 可以看到,这2个子协程是交替打印输出的, 这两个子协程是并行的。
* 同时,由于协程的 delay() 方法是非阻塞的,所以,即使一个子协程会先执行 delay(100L),但它也并不会阻塞另一个子协程delay(90L) 的运行。
*
* */
案例6:阻塞案例:协程 + sleep【sleep 是阻塞的】
由此可见,Kotlin 协程的
非阻塞
其实只是语言层面
的,当我们调用 JVM 层面的 Thread.sleep() 的时候,它仍然会变成阻塞式
的。所以,在协程当中应该尽量避免出现阻塞式的行为。即尽量使用 delay,而不是 sleep。
private fun coroutines_sleep_test() {
runBlocking {
// 创建一个子协程
launch {
repeat(3) { // 重复打印三次
Thread.sleep(100L) // sleep 是阻塞的
Log.e(TAG, "Print-1: ${Thread.currentThread().name}")
}
}
// 创建一个子协程
launch {
repeat(3) { // 重复打印三次
Thread.sleep(90L) // sleep 是阻塞的
Log.e(TAG, "Print-2: ${Thread.currentThread().name}")
}
}
}
}
/** 由此可见,Kotlin 协程的非阻塞其实只是语言层面的,当我们调用 JVM 层面的 Thread.sleep() 的时候,它仍然会变成阻塞式的。
* 所以,在协程当中应该尽量避免出现阻塞式的行为。即尽量使用 delay,而不是 sleep。
* Print-1: main
* Print-1: main
* Print-1: main
* Print-2: main
* Print-2: main
* Print-2: main
* */
Kotlin协程思维模型
一个线程中能运行成千上万的协程,那么操作系统和 CPU 是如何调度的呢?毕竟操作系统和 CPU 对协程是无感知的。
猜测:协程本质上只是封装线程的框架,底层还是线程,效率并没有超越线程,只是让我们程序员使用的得更方便而已。
所以:协程只是比乱用线程要高效,但是和合理使用线程池的效率是一致的。
挂起函数串行执行
在协程体中 , 连续使用多个挂起函数 , 这些挂起函数的执行是顺序执行的 , 挂起函数 1 执行完毕后 , 才执行 挂起函数 2 ;
案例:
private fun suspendSerialExecution() {
runBlocking {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// measureTimeMillis 函数用于测量内部代码块执行的时间, 单位毫秒 ms
val time = measureTimeMillis{
val ret1 = hello1()
val ret2 = hello2()
Log.e(TAG, "两个返回值相加 ${ret1 + ret2}")
}
Log.e(TAG, "挂起函数执行耗时 ${time} ms") //
}
}
private suspend fun hello1(): Int {
delay(200)
Log.e(TAG, "挂起函数1执行")
return 1
}
private suspend fun hello2(): Int {
delay(300)
Log.e(TAG, "挂起函数2执行")
return 2
}
执行结果 : 最终执行结果为 502 ms ,
挂起函数并发执行
如果想要两个挂起函数并发执行 , 并且同时需要两个挂起函数的返回值 , 则使用 async 协程构建器 , 启动两个协程 , 在协程体中执行两个并发挂起函数 ;
案例:
private fun suspendConcurrentExecution() {
runBlocking {
// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
// measureTimeMillis 函数用于测量内部代码块执行的时间, 单位毫秒 ms
val time = measureTimeMillis {
//创建一个子协程
val ret1 = async {
hello1()
}
//创建第二个子协程
val ret2 = async {
hello2()
}
Log.e(TAG, "子协程返回值相加 ${ret1.await() + ret2.await()}")
}
Log.e(TAG, "挂起函数执行耗时 ${time} ms") // 挂起函数执行耗时 303 ms
}
}
private suspend fun hello1(): Int {
delay(200)
Log.e(TAG, "挂起函数1执行")
return 1
}
private suspend fun hello2(): Int {
delay(300)
Log.e(TAG, "挂起函数2执行")
return 2
}
执行结果 : 启动两个 async 子协程 , 并发执行两个挂起函数 , 耗时 303 ms , 达到了并发执行减少执行时间的目的 ;
协程取消
常见的4种协程取消方法 :
❶ 取消协程作用域 : 取消 协程作用域 会将该作用域中的 所有 子协程 一同取消 ;
❷ 取消子协程 : 子协程 的取消 不会影响 同一层级的 兄弟协程的执行 ;
❸ 通过抛出异常取消协程 : 协程取消通常会通过 抛出 CancellationException 异常 实现 ;
❹ 挂起函数取消 : 定义在 kotlinx.coroutines 包下的 suspend 挂起函数 是可以取消的 , 如 delay 函数
【协程作用域】不取消
案例1:协程作用域 不取消 会等所有的子协程执行完毕
private fun notCancelcoroutineScope() {
// 创建协程作用域
val job0 = CoroutineScope(Dispatchers.Default)
//可以创建一个子协程
.launch {
Log.e(TAG, "job0 子协程执行开始")
delay(2000)
Log.e(TAG, "job0 子协程执行完毕")
}
}
/**协程作用域 不取消 会等所有的子协程执行完毕
* job0 子协程执行开始
* job0 子协程执行完毕
* */
private fun notCancelcoroutineScope2() {
// 调用 runBlocking 函数 , 创建一个新的协程作用域
runBlocking {
Log.e(TAG, "当前所处的线程: ${Thread.currentThread().name} ") // 当前所处的线程: main
Log.e(TAG, "获取 协程在上下文指定的调度器:${this.coroutineContext}") // 获取 协程在上下文指定的调度器:[BlockingCoroutine{Active}@ced2f25, BlockingEventLoop@f888fa]
//coroutineScope是个suspend挂起函数 需要传入一个作用域参数block
coroutineScope(block = {
val job0:Job =this.launch {
Log.e(TAG, "job0 子协程执行开始")
delay(3000)
Log.e(TAG, "job0 子协程执行完毕")
}
val async1 : Deferred<String> = this.async {
Log.e(TAG, "async1 子协程执行开始")
delay(3000)
Log.e(TAG, "async1 子协程执行完毕")
"123"
}
})
val coroutineScope1 = CoroutineScope(Dispatchers.Default)
val job2 = coroutineScope1.launch {
Log.e(TAG, "job2 子协程执行开始")
delay(2000)
Log.e(TAG, "job2 子协程执行完毕")
}
val job3= coroutineScope1.launch {
Log.e(TAG, "job3 子协程执行开始")
delay(2000)
Log.e(TAG, "job3 子协程执行完毕")
}
val coroutineScope2 = CoroutineScope(Dispatchers.Default)
val job4 = coroutineScope2.launch {
Log.e(TAG, "job4 子协程执行开始")
delay(1000)
Log.e(TAG, "job4 子协程执行完毕")
}
delay(500)
}
}
/**协程作用域 不取消 会等所有的子协程执行完毕
当前所处的线程: main
获取 协程在上下文指定的调度器:[BlockingCoroutine{Active}@ced2f25, BlockingEventLoop@f888fa]
job0 子协程执行开始
async1 子协程执行开始
job0 子协程执行完毕
async1 子协程执行完毕
job3 子协程执行开始
job2 子协程执行开始
job4 子协程执行开始
job4 子协程执行完毕
job3 子协程执行完毕
job2 子协程执行完毕
*/
【协程作用域】取消
案例1:取消 该父协程作用域之后 , 该作用域下的 所有子协程都会被取消了,
private fun cancelcoroutineScope1() {
runBlocking {
//首先 , 创建协程作用域 ;
val coroutineScope = CoroutineScope(Dispatchers.Default)
//然后 , 在协程作用域中 创建两个子协程 ;
val async1: Deferred<String> = coroutineScope.async{
Log.e(TAG, "async1 子协程执行开始 isActive=${isActive}")
delay(2000)
Log.e(TAG, "async1 子协程执行完毕 isActive=${isActive}")
"123"
}
val async2: Deferred<String> = coroutineScope.async{
Log.e(TAG, "async2 子协程执行开始 isActive=${isActive}")
delay(2000)
Log.e(TAG, "async2 子协程执行完毕 isActive=${isActive}")
"456"
}
// 100ms 后取消协程作用域
delay(100)
// 取消协程作用域
coroutineScope.cancel()
Log.e(TAG, "输出 coroutineScope父协程作用域的状态 coroutineScope.isActive=${coroutineScope.isActive}")
Log.e(TAG, "输出 async1 子协程的状态 async1.isActive=${async1.isActive}")
Log.e(TAG, "输出 async2 子协程的状态 async2.isActive=${async2.isActive}")
Log.e(TAG, "输出 async1 子协程的结果值 ${async1.await()}") //这里无法输出 取消 coroutineScope 协程作用域之后 , 该作用域下的 async1 子协程 被取消了 不会执行
Log.e(TAG, "输出 async2 子协程的结果值 ${async2.await()}") //这里无法输出 取消 coroutineScope 协程作用域之后 , 该作用域下的 async2 子协程 被取消了 不会执行
Log.e(TAG,"程序结束")
}
}
/**
*取消 coroutineScope 父协程作用域之后 , 该作用域下的 async1 和 async2 子协程都被取消了 , 两个子协程都没有执行完毕 ;
* async1 子协程执行开始 isActive=true
* async2 子协程执行开始 isActive=true
* 输出 coroutineScope父协程作用域的状态 coroutineScope.isActive=false
* 输出 async1 子协程的状态 async1.isActive=false
* 输出 async2 子协程的状态 async2.isActive=false
* */
案例2:coroutineScope 协程作用域的子协程async1如果使用await()/join(),该子协程必须先执行完毕后,才会执行后面的代码,在coroutineScope 协程作用域取消之后,该作用域下的 其他的async2 子协程 就会被取消了 不会执行
private fun cancelcoroutineScope2() {
runBlocking {
//首先 , 创建协程作用域 ;
val coroutineScope = CoroutineScope(Dispatchers.Default)
//然后 , 在协程作用域中 创建两个子协程 ;
val async1: Deferred<String> = coroutineScope.async{
Log.e(TAG, "async1 子协程执行开始")
delay(2000)
Log.e(TAG, "async1 子协程执行完毕")
"123"
}
Log.e(TAG, "输出async1 子协程的结果值 ${async1.await()}") //子协程的join await 很牛逼 必须先让自身async1子协程逻辑执行完了 才会执行后面的逻辑
val async2: Deferred<String> = coroutineScope.async{
Log.e(TAG, "async2 子协程执行开始")
delay(1000)
Log.e(TAG, "async2 子协程执行完毕") //不会输出 coroutineScope 协程作用域取消了
"456"
}
// 100ms 后取消协程作用域
delay(100)
Log.e(TAG, "coroutineScope取消了")
// 取消协程作用域
coroutineScope.cancel()
Log.e(TAG, "输出 async2 子协程的结果值 ${async2.await()}") //这里无法输出 取消 coroutineScope 协程作用域之后 , 该作用域下的 async2 子协程 被取消了 不会执行
}
}
/**
* coroutineScope 协程作用域的子协程async1如果使用await()/join(),该子协程必须先执行完毕后,才会执行后面的代码,
* 在coroutineScope 协程作用域取消之后
* 该作用域下的 其他的async2 子协程 就会被取消了 不会执行
async1 子协程执行开始
async1 子协程执行完毕
输出async1 子协程的结果值 123
async2 子协程执行开始
coroutineScope取消了
* */
【子协程】取消
单独取消 协程作用域 中的 子协程 , 协程作用域 中的其它 兄弟协程不受影响 ;
案例1:在 协程作用域 coroutineScope 中 启动了 job0 和 job1 两个协程 , 取消了 job1 协程 , job1 协程没有执行完毕 , job0 协程执行完毕 ;
private fun launchCancel() {
runBlocking {
// 创建协程作用域coroutineScope
val coroutineScope = CoroutineScope(Dispatchers.Default)
//创建该协程作用域的子协程 job1
val job1 : Job = coroutineScope.launch{
Log.e(TAG, "job1 子协程执行开始")
delay(2000)
Log.e(TAG, "job1 子协程执行完毕")
}
//创建该协程作用域的子协程 job2
val job2 : Job = coroutineScope.launch{
Log.e(TAG, "job2 子协程执行开始")
delay(2000)
Log.e(TAG, "job2 子协程执行完毕")
}
delay(100)
// 取消协程作用域中的子协程
job1.cancel()
}
}
/** 在 协程作用域 coroutineScope 中 启动了 job1 和 job2 两个协程 , 取消了 job1 协程 , job1 协程没有执行完毕 , job2 协程执行完毕 ;
* job1 子协程执行开始
* job2 子协程执行开始
* job2 子协程执行完毕
* */
【默认异常】取消子协程
// 取消协程作用域中的子协程
job1.cancel()
案例1:
private fun cancel_defalut(){
runBlocking {
// 创建协程作用域
val coroutineScope = CoroutineScope(Dispatchers.Default)
//在该作用域下创建一个子协程
val job1 = coroutineScope.launch {
try {
Log.e(TAG, "job1 子协程执行开始")
delay(2000)
Log.e(TAG, "job1 子协程执行完毕") //不会输出 子协程抛出异常了 子协程不会执行完毕
}catch (e: Exception) {
Log.e(TAG, "job1 子协程执行捕获到异常 : ${e.toString()}")
e.printStackTrace()
}
}
// 100ms 后取消协程作用域
delay(100)
// 取消协程作用域中的子协程
job1.cancel()
}
}
/**
* job1 子协程执行开始
* job1 子协程执行捕获到异常 : kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@335547a
*
* */
【自定义异常】取消子协程
job1.cancel(CancellationException("自定义 CancellationException 异常"))
案例1:
private fun cancel_CancellationException() {
runBlocking {
//创建一个协程作用域
val coroutineScope = CoroutineScope(Dispatchers.Default)
//在该作用域下创建一个子协程
val job1 = coroutineScope.launch {
try {
Log.e(TAG, "job1 子协程执行开始")
delay(2000)
Log.e(TAG, "job1 子协程执行完毕") //不会输出 子协程抛出异常了 子协程不会执行完毕
}catch (e: Exception) {
Log.e(TAG, "job1 子协程执行捕获到异常 : ${e.toString()}")
e.printStackTrace()
}
}
// 100ms 后取消协程作用域
delay(100)
//在调用 cancel()函数取消协程时, 可以传入一个 CancellationException 实例来提供更多关于本次取消的详细信息
//如果您不构建新的 CancellationException 实例将其作为参数传入的话,会创建一个默认的 CancellationException
// 取消协程作用域中的子协程 并抛出具体的异常信息
job1.cancel(CancellationException(" 抛出自定义 CancellationException 异常 "))
}
}
/**
* job1 子协程执行开始
* job1 子协程执行捕获到异常 : java.util.concurrent.CancellationException: 抛出自定义 CancellationException 异常
*
* */
【 CPU 密集型协程任务】取消
1.CPU 密集型协程任务 , 是无法 直接取消
CPU 密集型协程任务 , 是无法 直接取消的 ; 此类任务一直在 抢占 CPU 资源 , 使用 cancel 函数 , 无法取消该类型的 协程任务 ;
在进行 CPU 密集计算时 , 中间会有大量的中间数据 , 如果中途取消 , 大量的临时数据会丢失 , 因此在协程中 , 无法直接取消 CPU 密集型协程任务 , 这是对协程的保护措施 ;
案例1:在执行子协程任务过程中 , 子协程调用 cancel()
后,子协程任务并不会被取消。
因为虽然此时协程已经不是活跃状态了,但协程内部的代码不会主动响应此状态,因此协程就无法真正取消。
private fun CPU_Coroutine_cancel() {
runBlocking {
val job1 = CoroutineScope(Dispatchers.Default).launch {
Log.e(TAG, "子协程job1任务执行开始")
var i = 0
while (i<=10000){
i++
if(i == 10000) {
delay(10)
Log.e(TAG, "最后一次循环 : i = ${i}")
Log.e(TAG, "子协程job1任务执行完毕")
}
}
}
job1.invokeOnCompletion { Log.e(TAG, " job1子协程结束监听事件回调") }
// 10ms 后取消协程作用域
delay(10)
Log.e(TAG, "取消子协程job1任务")
// 取消协程任务
// job1.cancel() // 取消该作业
// job1.join() // 等待作业执行结束
job1.cancelAndJoin()
Log.e(TAG, "看看当前子协程job1状态isActive="+job1.isActive) // false,此时协程已经不是活跃状态了
Log.e(TAG, "退出协程作用域")
}
}
/**在执行子协程任务过程中 , 取消子协程 , 但是没有取消成功 , 子协程自动执行完毕 ;
*
*子协程job1任务执行开始
取消子协程job1任务
* 最后一次循环 : i = 10000
* 子协程job1任务执行完毕
* job1子协程结束监听事件回调
* 看看当前子协程job1状态isActive=false
* 退出协程作用域
* */
2. 【使用 isActive=false 】需要主动判断当前 (CPU 密集型)子协程是否是活跃状态
❶当协程 处于 活跃 Active 状态 时 :
isActive=true
❷当调用 Job#cancel() 函数取消协程时 , 当前协程的任务会变为 取消中 Cancelling 状态 :
isActive == false && isCancelled == true ;
❸当所有的子协程执行完毕会后 , 协程会进入 已取消 Cancelled 状态 :
isCompleted == true
案例1: 把 while 循环的条件改成 while (isActive)
,即只有协程处于活跃状态(isActive=true
),才继续执行while循环体内部的代码。如果协程处于活跃状态(isActive=fasle
) , 则while (fasle)
终止while循环。
private fun CPU_Coroutine_cancel2() {
runBlocking {
val job1 = CoroutineScope(Dispatchers.Default).launch {
Log.e(TAG, "协程任务执行开始 isActive=$isActive")
var i = 0
//每次 while循环时, 都会判断只有isActive=true 协程活跃运行状态(协程没有取消)才会进入打印
while (i<=10000000&&isActive){
i++
Log.e(TAG, "i= $i isActive=$isActive")
if(i == 10000000) {
Log.e(TAG, "最后一次循环 : i = ${i}")
Log.e(TAG, "子协程job1任务执行完毕")
}
}
}
job1.invokeOnCompletion { Log.e(TAG, " job1子协程结束监听事件回调,获取其活跃状态isActive=${job1.isActive}") }
// 10ms 后取消协程作用域
delay(10)
Log.e(TAG, "取消子协程job1任务前, 其活跃状态isActive=${job1.isActive}")
// 取消子协程job1任务后,其活跃状态isActive=fasle
job1.cancelAndJoin()
Log.e(TAG, "取消子协程job1任务后,其活跃状态isActive=${job1.isActive}")
Log.e(TAG, "退出协程作用域 ")
}
}
/**
* 协程任务执行开始 isActive=true
i= 1 isActive=true
i= 2 isActive=true
i= 3 isActive=true
...
取消子协程job1任务前, 其活跃状态isActive=true
job1子协程结束监听事件回调,获取其活跃状态isActive=false
取消子协程job1任务后,其活跃状态isActive=false
退出协程作用域
*/
3.【调用 ensureActive函数】 自动处理协程退出
在协程中 , 可以执行 ensureActive()
函数 , 在该函数中会 自动判定当前协程的活跃 isActive 状态 , 如果当前协程处于取消中状态 , 自动抛出 CancellationException 异常 , 并退出协程 ;
public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive()
public fun Job.ensureActive(): Unit {
if (!isActive) throw getCancellationException()
}
案例1:协程中执行的循环任务 , 每次while循环时 , 都调用一次 ensureActive() 函数 , 判断当前协程是否已经取消 , 如果协程已经取消则抛出异常(会造成程序的崩溃) , 退出协程 ,取消协程执行;
private fun CPU_Coroutine_cancel3() {
runBlocking {
val job1 = CoroutineScope(Dispatchers.Default).launch {
Log.e(TAG, "协程任务执行开始 isActive=$isActive")
var i = 0
//协程中执行的循环任务 , 每次while循环时 , 都调用一次 ensureActive() 函数 , 判断当前协程是否已经取消 ,
// 如果协程已经取消则抛出异常(会造成程序的崩溃) , 退出协程 ,取消协程执行;
while (i<=10000000){
ensureActive()
i++
Log.e(TAG, "i= $i isActive=$isActive")
if(i == 10000000) {
Log.e(TAG, "最后一次循环 : i = ${i}")
Log.e(TAG, "协程任务执行完毕")
}
}
}
// 10ms 后取消协程作用域
delay(10)
Log.e(TAG, "取消子协程job1任务 isActive=$isActive")
// 取消协程任务
job1.cancelAndJoin()
Log.e(TAG, "退出协程作用域 isActive=$isActive")
}
}
/**
* 协程任务执行开始 isActive=true
* i= 1 isActive=true
* i= 2 isActive=true
* i= 3 isActive=true
* ...
* 取消子协程job1任务 isActive=true
* i= 95 isActive=true
* i= 96 isActive=true
* i= 97 isActive=true
* ...
* 退出协程作用域 isActive=true
*
* */
4.【调用 yield 函数】检查协程状态并处理协程取消操作
在协程中 , 可以使用 yield() 函数 , 检查当前协程的状态 , 如果已经调用 cancel() 函数取消协程 , 则抛出 CancellationException 异常 , 取消协程 ;
yield() 函数 比 ensureActive 函数 更加复杂 , 该函数还尝试出让线程执行权 , 将执行权让给别的协程执行 ; yield() 函数 会在每次循环时 , 都执行一次 , 每次循环时都执行该函数的时候 , 此时会尝试出让线程的执行权 , 看看是否有其它更紧急的协程需要执行 , 如果有 , 则让其它协程先执行 ;
yield()
函数每次执行前都问一下其它协程 , 你们需要执行吗 , 如果需要先让你们执行一次 ;这样可以避免 协程的 CPU 占用太密集 , 导致其它协程无法执行 的情况 ;
案例1:
private fun CPU_Coroutine_cancel3_yield() {
runBlocking {
val job1 = CoroutineScope(Dispatchers.Default).launch {
Log.e(TAG, "协程任务执行开始 isActive=$isActive")
var i = 0
//使用 yield() 函数 , 检查当前协程的状态 , 如果已经调用 cancel() 函数取消协程 //, 则抛出 CancellationException 异常 , 取消协程 ;
while (i<=10000000){
yield()
i++
Log.e(TAG, "i= $i isActive=$isActive")
if(i == 10000000) {
Log.e(TAG, "最后一次循环 : i = ${i}")
Log.e(TAG, "协程任务执行完毕")
}
}
}
// 10ms 后取消协程作用域
delay(10)
Log.e(TAG, "取消子协程job1任务 isActive=$isActive")
// 取消协程任务
job1.cancelAndJoin()
Log.e(TAG, "退出协程作用域 isActive=$isActive")
}
}
/**
* 协程任务执行开始 isActive=true
* i= 1 isActive=true
* i= 2 isActive=true
* i= 3 isActive=true
* ...
* 取消子协程job1任务 isActive=true
* i= 95 isActive=true
* i= 96 isActive=true
* i= 97 isActive=true
* ...
* 退出协程作用域 isActive=true
*
* */
【 释放协程资源】
如果 协程中途取消 , 期间需要 释放协程占有的资源 ;
如果执行的协程任务中 , 需要 执行 关闭文件 , 输入输出流 等操作 , 推荐使用 try…catch…finally 代码块 , 即使是协程取消时 , 在 finally 代码块中的代码 , 也会执行 ;
案例1:即使是取消协程任务后 , 在协程抛出 JobCancellationException 异常后 , finally 中的代码也会执行,可进行资源的释放 ;
private fun CPU_Coroutine_cancel3_finally() {
runBlocking {
val job1 = CoroutineScope(Dispatchers.Default).launch {
try {
Log.e(TAG, "协程任务执行开始 isActive=$isActive")
var i = 0
//使用 yield() 函数 , 检查当前协程的状态 , 如果已经调用 cancel() 函数取消协程 , 则抛出 CancellationException 异常 , 取消协程 ;
while (i <= 10000000) {
yield()
i++
Log.e(TAG, "i= $i isActive=$isActive")
if (i == 10000000) {
Log.e(TAG, "最后一次循环 : i = ${i}")
Log.e(TAG, "协程任务执行完毕")
}
}
} catch (e: Exception) {
Log.e(TAG, "协程取消抛出异常: $e")
e.printStackTrace()
} finally {
//即使是取消协程任务后 , 在协程抛出 JobCancellationException 异常后 , finally 中的代码也会执行,可进行资源的释放 ;
Log.e(TAG, "即使子协程取消,finally也会执行,释放协程占用的资源 ")
}
}
// 10ms 后取消协程作用域
delay(10)
Log.e(TAG, "取消子协程job1任务 isActive=$isActive")
// 取消协程任务
job1.cancelAndJoin()
Log.e(TAG, "退出协程作用域 isActive=$isActive")
}
}
/**
* 协程任务执行开始 isActive=true
* i= 1 isActive=true
* i= 2 isActive=true
* i= 3 isActive=true
* ...
* 取消子协程job1任务 isActive=true
* i= 13 isActive=true
* i= 14 isActive=true
协程取消抛出异常: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@89505a9
即使子协程取消,finally也会执行,释放协程占用的资源
退出协程作用域 isActive=true
*
* */
【使用 withContext(NonCancellable) 】
创建/构造一个 【无法取消的协程任务】
如果在协程取消后 , finally 代码块的代码肯定会执行 , 如果在 finally 中需要使用 suspend 挂起函数(delay()) , 则 suspend挂起函数(delay())以及之后的代码将不会被执行 ;
那怎么办呢?怎么解决这个问题呢?
使用 withContext(NonCancellable) {}
代码块 , 可以构造一个无法取消的协程任务 , 这样可以避免 finally 中的代码无法完全执行 ;
案例1:如果在 finally 中需要使用 suspend 挂起函数(delay()) , 则 suspend挂起函数(delay())以及之后的代码将不会被执行 ;
private fun CPU_Coroutine_cancel3_finally_suspendMethed() {
runBlocking {
val job1 = CoroutineScope(Dispatchers.Default).launch {
try {
Log.e(TAG, "协程任务执行开始 isActive=$isActive")
var i = 0
//使用 yield() 函数 , 检查当前协程的状态 , 如果已经调用 cancel() 函数取消协程 , 则抛出 CancellationException 异常 , 取消协程 ;
while (i <= 10000000) {
yield()
i++
Log.e(TAG, "i= $i isActive=$isActive")
if (i == 10000000) {
Log.e(TAG, "最后一次循环 : i = ${i}")
Log.e(TAG, "协程任务执行完毕")
}
}
} catch (e: Exception) {
Log.e(TAG, "协程取消抛出异常: $e")
e.printStackTrace()
} finally {
//即使是取消协程任务后 , 在协程抛出 JobCancellationException 异常后 , finally 中的代码也会执行,可进行资源的释放 ;
Log.e(TAG, "即使子协程取消,finally也会执行,释放协程占用的资源开始 ")
delay(1000)
//如果在 finally 中需要使用 suspend 挂起函数(delay()) , 则 suspend挂起函数(delay())以及之后的代码将不会被执行 ;
Log.e(TAG, "释放协程占用的资源完毕") //无法输出
}
}
// 10ms 后取消协程作用域
delay(10)
Log.e(TAG, "取消子协程job1任务 isActive=$isActive")
// 取消协程任务
job1.cancelAndJoin()
Log.e(TAG, "退出协程作用域 isActive=$isActive")
}
}
/**
* 协程任务执行开始 isActive=true
* i= 1 isActive=true
* i= 2 isActive=true
* i= 3 isActive=true
* ...
* 取消子协程job1任务 isActive=true
* i= 13 isActive=true
* i= 14 isActive=true
协程取消抛出异常: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@89505a9
即使子协程取消,finally也会执行,释放协程占用的资源开始
退出协程作用域 isActive=true
* */
案例2:finally 代码块中存在挂起函数 , 但是整个代码块被 withContext(NonCancellable) 代码块包裹 , 所有的代码执行完毕,即使有suspend挂起函数也会执行 ;
private fun CPU_Coroutine_cancel3_finally_withContext_NonCancellable() {
runBlocking {
val job1 = CoroutineScope(Dispatchers.Default).launch {
try {
Log.e(TAG, "协程任务执行开始 isActive=$isActive")
var i = 0
//使用 yield() 函数 , 检查当前协程的状态 , 如果已经调用 cancel() 函数取消协程 , 则抛出 CancellationException 异常 , 取消协程 ;
while (i <= 10000000) {
yield()
i++
Log.e(TAG, "i= $i isActive=$isActive")
if (i == 10000000) {
Log.e(TAG, "最后一次循环 : i = ${i}")
Log.e(TAG, "协程任务执行完毕")
}
}
} catch (e: Exception) {
Log.e(TAG, "协程取消抛出异常: $e")
e.printStackTrace()
} finally {
//如果在 finally 中需要使用 suspend 挂起函数(delay()) , 则 suspend挂起函数(delay())以及之后的代码将不会被执行 ;
//finally 代码块中存在挂起函数 , 但是整个代码块被 withContext(NonCancellable) 代码块包裹 , 所有的代码执行完毕 ;
withContext(NonCancellable) {
//即使是取消协程任务后 , 在协程抛出 JobCancellationException 异常后 , finally 中的代码也会执行,可进行资源的释放 ;
Log.e(TAG, "即使子协程取消,finally也会执行,释放协程占用的资源开始 ") //一定输出
delay(1000)
Log.e(TAG, "释放协程占用的资源完毕") //一定输出
}
}
}
// 10ms 后取消协程作用域
delay(10)
Log.e(TAG, "取消子协程job1任务 isActive=$isActive")
// 取消协程任务
job1.cancelAndJoin()
Log.e(TAG, "退出协程作用域 isActive=$isActive")
}
}
/**
* 协程任务执行开始 isActive=true
* i= 1 isActive=true
* i= 2 isActive=true
* i= 3 isActive=true
* ...
* 取消子协程job1任务 isActive=true
* i= 13 isActive=true
* i= 14 isActive=true
协程取消抛出异常: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@89505a9
即使子协程取消,finally也会执行,释放协程占用的资源开始
释放协程占用的资源完毕
退出协程作用域 isActive=true
* */
【使用 withTimeout
(timeMillis: Long) 函数 】创建/构造 一个超时取消的协程任务.
如果在规定的时间内没有超时,就会返回withTimeout(){}协程任务的正常结果值
如果超时 规定时间timeMillis 就会自动取消 , 如果超时则报kotlinx.coroutines.TimeoutCancellationException
异常信息 ; 没有返回结果值
案例1:
private fun Coroutine_cancel_withTimeout_in() {
runBlocking {
// 构造超时取消的协程任务
val result:String=withTimeout(5000){
//在规定时间5000内,协程任务执行完毕
Log.e(TAG, "协程任务执行开始")
delay(4000)
Log.e(TAG, "协程任务执行完毕")
"okk"
}
Log.e(TAG, "输出上述协程任务的返回值为: $result")
}
}
/**
* 协程任务执行开始
* 协程任务执行完毕
* 输出上述协程任务的返回值为: okk
* */
案例2:
private fun Coroutine_cancel_withTimeout_out() {
runBlocking {
// 构造超时取消的协程任务
val result:String=withTimeout(5000){
//在规定时间5000内,协程任务执行完毕
Log.e(TAG, "协程任务执行开始")
delay(7000)
Log.e(TAG, "协程任务执行完毕")//超时不会输出
"okk"
}
Log.e(TAG, "输出上述协程任务的返回值为: $result") //超时不会输出
}
}
/**
* 协程任务执行开始
* java.lang.RuntimeException: Unable to start activity ComponentInfo{com.my.runalone_coroutinedemo/com.my.runalone_coroutinedemo.MainActivity}:
* kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 5000 ms
*
* */
【使用 withTimeoutOrNull(timeMillis: Long) 函数 】创建/构造 一个超时取消的协程任务.
如果在规定的时间内没有超时,就会返回withTimeoutOrNull{}协程任务的正常结果值
如果超时 规定时间timeMillis ,就会自动取消协程 ,同时会返回结果值null
案例1:
private fun Coroutine_cancel_withTimeoutOrNull_in() {
runBlocking {
// 构造超时取消的协程任务
val result:String?=withTimeoutOrNull(5000){
//在规定时间5000内,协程任务执行完毕
Log.e(TAG, "协程任务执行开始")
delay(4000)
Log.e(TAG, "协程任务执行完毕")
"okk"
}
Log.e(TAG, "输出上述协程任务的返回值为: $result")
}
}
/**
* 协程任务执行开始
* 协程任务执行完毕
* 输出上述协程任务的返回值为: okk
*
* */
案例2:
private fun Coroutine_cancel_withTimeoutOrNull_out() {
runBlocking {
// 构造超时取消的协程任务
val result:String?=withTimeoutOrNull(5000){
//在规定时间5000内,协程任务执行完毕
Log.e(TAG, "协程任务执行开始")
delay(7000)
Log.e(TAG, "协程任务执行完毕")//超时不会输出
// 执行完毕后的返回值
// 如果超时则返回 null 不会返回 "okk"
"okk"
}
Log.e(TAG, "输出上述协程任务的返回值为: $result") // 输出上述协程任务的返回值为: null
}
}
/**
* 协程任务执行开始
* 输出上述协程任务的返回值为: null
*
* */
协程异常处理
在 协程任务 中 , 执行的代码出现异常 , 需要进行 异常处理 , 并给出错误提示信息 , 展示给用户 或者 上报服务器 ;
协程构建器 有两种 异常处理 形式 :
1.自动传播异常 :
使用 launch 或 async 构建器 创建的 根协程 , 如果出现异常 , 会 马上抛出异常 ; 此类异常 在 可能出现异常的代码位置 进行捕获即可 ;
2.向用户暴露异常 :
使用 async 或 produce 构建器 创建的 根协程 , 如果出现异常 , 则需要 用户 通过 await 或 receive 来处理异常
根协程【自动传播异常】
自动传播异常 : 使用 launch 或 async 构建器 创建的 根协程 , 如果出现异常 , 会 马上抛出异常 ; 此类异常 在 可能出现异常的代码位置 进行捕获即可 ;
注意 : 下面讨论的情况是 根协程 的异常传播 ;
1.根协程体抛出异常
launch 构建器 异常代码示例 : 使用 launch 构建器创建协程 , 在协程任务中抛出异常 , 查看异常的抛出点 ;
案例1:throw ArithmeticException()
位置出现异常 , 说明 launch 构建器创建的协程抛出的异常是立即抛出 , 捕获异常也应该在该位置进行捕获 ;
private fun arithmeticException() {
runBlocking {
//throw ArithmeticException() 位置出现异常 , 说明 launch 构建器创建的协程抛出的异常是立即抛出 , 捕获异常也应该在该位置进行捕获 ;
val job1 = GlobalScope.launch() {
throw ArithmeticException()
}
job1.join()
}
}
2.在协程体捕获异常
在协程任务代码中可能抛出异常的代码处捕获异常 ;
private fun arithmeticException2() {
runBlocking {
//throw ArithmeticException() 位置出现异常 , 说明 launch 构建器创建的协程抛出的异常是立即抛出 , 捕获异常也应该在该位置进行捕获 ;
val job1 = GlobalScope.launch() {
try {
throw ArithmeticException()
}catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "launch协程捕获异常: ${e.toString()} ") //launch协程捕获异常: java.lang.ArithmeticException
}
}
job1.join()
}
}
/**
* launch协程捕获异常: java.lang.ArithmeticException
* */
根协程【向用户暴露异常】
向用户暴露异常 : 使用 async 或 produce 构建器 创建的 根协程 , 如果出现异常 , 则需要 用户 通过 await 或 receive 来处理异常 ;
注意 : 下面讨论的情况是 根协程 的异常传播 ;
1.在根协程的 await、receive 处抛出异常
案例1:如果不调用 async 构建的 Deferred 协程任务 的 await 方法 , 则不会报异常 ;
private fun arithmeticException3() {
runBlocking {
val deferred = GlobalScope.async () {
throw ArithmeticException()
}
}
}
// 每调用 await() 没有异常抛出
2.在根协程的 await、receive 处捕获异常
案例1:在根协程在deferred.await()
代码处捕获异常 ;
private fun arithmeticException4() {
runBlocking {
val deferred = GlobalScope.async {
throw ArithmeticException()
}
try {
deferred.await()
}catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "async协程在await()处捕获异常: ${e.toString()} ") //async协程在await()处捕获异常: java.lang.ArithmeticException
}
}
}
/**
* async协程在await()处捕获异常: java.lang.ArithmeticException
* */
非根协程【异常处理】
非根协程 , 也就是 子协程 的异常 会被传播 ;
案例:在子协程中产生的异常 , 会直接抛出 ;
private fun arithmeticException5() {
runBlocking {
val job = GlobalScope.launch {
// 非根协程产生的异常, 直接抛出
throw ArithmeticException()
}
job.join()
}
}
异常传播特性
协程 运行时 , 产生异常 , 会将异常 传递给 父协程 , 父协程会执行如下操作 :
-
① 取消子协程 : 不仅仅取消产生异常的子协程 , 该父协程下所有的子协程都会取消 ;
- ② 取消父协程 : 将父协程本身取消 ;
- ③ 向父协程的父协程传播异常 : 继续将异常传播给 父协程的父协程 ;
这样就会导致 某个子协程一旦出现异常 , 则 兄弟协程 , 父协程 , 父协程的兄弟协程 , 父协程的父协程 等等 都会被取消 , 这样牵连太大 , 因此本篇博客中引入几种异常处理机制解决上述问题 ;
解决方法就是使用:SupervisorJob 协程(详细见SupervisorJob 协程模块讲解)
协程异常处理器 CoroutineExceptionHandler 捕获异常
异常捕获 : 在协程中 , 使用 CoroutineExceptionHandler 对协程运行过程中产生的 异常 进行捕获 , 异常满足如下两个条件才会被捕 :
❶异常捕获时机 : 协程 自动抛出 的异常 , 可以在协程内被捕获 ; 使用 launch 构建的协程 可以在协程中捕获异常 , 使用 async 构建的协程 在 await 处捕获异常 ;
❷异常捕获位置 : 在 协程作用域 CoroutineScope 或者在 根协程 中 捕获 异常 ;
在 Android 程序中 , 可以使用 协程异常处理器 CoroutineExceptionHandler 捕获异常 , 将其实例对象传递给 launch 协程构建器 作为参数即可 ;
异常处理器 coroutineExceptionHandler , 必须安装给 根协程 , 不能给内部子协程安装 ;如果将 异常处理器 coroutineExceptionHandler 设置给
CoroutineScope(Job())
协程作用域里的子协程 , 则异常不会被捕获到 ;
1、当协程任务出现复杂的嵌套层级
时,我们很难在每个协程体里面去写 try-catch,这时就要用到 CoroutineExceptionHandler
捕获异常。
private fun many_coroutine_nest_CoroutineExceptionHandler(){
runBlocking {
val handler = CoroutineExceptionHandler { _, throwable -> Log.e(TAG, "捕获异常信息 : $throwable") }
val scope = CoroutineScope(coroutineContext + Job() + handler)
scope.launch { launch { launch { 1 / 0 } } }
delay(1000L)
println("procedure End")
}
}
/**
* 捕获异常信息 : java.lang.ArithmeticException: divide by zero
* */
2、对比 launch 和 async 创建的协程的异常捕捉示例
代码示例 :
- 使用 launch 构造的协程 , 可以使用 CoroutineExceptionHandler 捕获异常 ;
- 使用 async 构造的协程 , 无法使用 CoroutineExceptionHandler 捕获异常 , 异常直接抛出 , 导致程序崩溃 ;
private fun launch_async_CoroutineExceptionHandler(){
// 创建 协程异常处理器 CoroutineExceptionHandler
val coroutineExceptionHandler = CoroutineExceptionHandler {
coroutineContext, throwable ->
Log.e(TAG, "CoroutineExceptionHandler处理子协程的异常: " + (coroutineContext[CoroutineName]?.name ?:"" )
+"\n协程上下文 $coroutineContext" + "\n异常内容 ${throwable.toString()}")
}
// 将主线程包装成协程
runBlocking {
// 创建子协程job , 传入 CoroutineExceptionHandler 作为协程上下文参数
val job = GlobalScope.launch (coroutineExceptionHandler+CoroutineName("子协程job")){
// 该异常会被捕获
throw AssertionError("子协程job 发生异常:空指针异常")
Log.e(TAG,"子协程job执行完毕")
}
// 创建子协程deferred , 传入 CoroutineExceptionHandler 作为协程上下文参数
val deferred = GlobalScope.async(coroutineExceptionHandler+CoroutineName("子协程deferred")) {
// 该异常不会被捕获
throw AssertionError("子协程deferred 发生异常:角标越界")
Log.e(TAG,"子协程deferred执行完毕")
}
// 等待 job 执行完毕
job.join()
// 等待 deferred 执行完毕
deferred.await()
}
}
/** 捕获到了 launch 创建的创建子协程job中的异常 , 但是 async 创建的子协程deferred中的异常直接抛出导致程序崩溃 ;
*
* CoroutineExceptionHandler处理子协程的异常: 子协程job
* 协程上下文 [com.my.runalone_coroutinedemo.MainActivity$launch_async_CoroutineExceptionHandler$$inlined$CoroutineExceptionHandler$1@920782b, CoroutineName(子协程job), StandaloneCoroutine{Cancelling}@3b27488, Dispatchers.Default]
* 异常内容 java.lang.AssertionError: 子协程job 发生异常:空指针异常
* */
3、验证 CoroutineScope 协程的异常捕捉示例
在使用 CoroutineExceptionHandler 对协程运行过程中产生的 异常 进行捕获 时 , 异常捕获的位置 只能是 协程作用域 CoroutineScope 或者在 根协程 中 ;
在上面的小节验证了 异常捕获位置 在根协程 中的情况 , 在本小节示例中 , 验证在 协程作用域 CoroutineScope 中捕获异常 ;
代码示例 :捕获到了在 CoroutineScope 创建的协程的子协程中抛出异常 ;
在 协程作用域 中 , 使用 launch 协程构建器 创建协程 , 传入 CoroutineExceptionHandler 实例对象参数 , 在其中再创建子协程 , 抛出异常 , 最终可以捕获到在子协程中抛出的异常 ;
下面代码中 创建协程作用域 时 , 使用的
CoroutineScope(Job())
进行创建 , 不是SupervisorJob()
, 因此 在子协程中抛出的异常 , 会传递给父协程 , 由父协程处理异常 .父协程创建时使用的 val father_job = scope.launch(coroutineExceptionHandler) 代码 , 在协程构建器中传入了 协程异常处理器 coroutineExceptionHandler, 因此该协程异常处理器 可捕获 子协程传递给父协程的异常 ;
异常处理器 coroutineExceptionHandler , 必须安装给 根协程 , 不能给内部子协程安装 ;如果将 异常处理器 coroutineExceptionHandler 设置给
CoroutineScope(Job())
协程作用域里的子协程 , 则异常不会被捕获到 ;
private fun CoroutineScope_CoroutineExceptionHandler(){
// 创建 协程异常处理器 CoroutineExceptionHandler
val coroutineExceptionHandler = CoroutineExceptionHandler {
coroutineContext, throwable ->
Log.e(TAG, "CoroutineExceptionHandler处理子协程的异常: " + (coroutineContext[CoroutineName]?.name ?:"" )
+"\n协程上下文 $coroutineContext" + "\n异常内容 ${throwable.toString()}")
}
// 将主线程包装成协程
runBlocking {
// 验证 在 协程作用域 CoroutineScope 中发生异常,使用 CoroutineExceptionHandler 捕获异常
val scope = CoroutineScope(Job())
//创建一个父协程
val father_job = scope.launch(coroutineExceptionHandler) {
// 创建一个子协程son_job
//val son_job = launch (coroutineExceptionHandler){ //todo 这种写法是错误的
val son_job = launch {
throw IllegalArgumentException("子协程son_job空指针异常")
Log.e(TAG,"子协程son_job执行完毕")
}
}
// 等待 父协程 father_job 执行完毕
father_job.join()
}
}
Android 全局异常处理器
Android 中的 全局异常处理器 , 可以 获取 所有的 协程 中产生的 没有被捕获的异常 ;
❶无法阻止崩溃 : 全局异常处理器 不能捕获这些异常 进行处理 , 应用程序 还是要崩溃 ;
❷用于调试上报 : 全局异常处理器 仅用于 程序调试 和 异常上报 场景 , 也就是出现了异常 , 将异常通知开发者 ;
全局异常处理器使用步骤如下 :
① 在 app/main/ 目录下创建 resources 目录 , 在 resources 目录下创建 META-INF 目录 ,
② 在 META-INF 目录下创建 services 目录 ,
③ 在 app/main/resources/META-INF/services 目录下 , 创建 名称为 kotlinx.coroutines.CoroutineExceptionHandler 的文件 ;
④ 创建 协程的 全局异常处理器 MyCoroutineExceptionHandler 自定义类 , 需要 实现 CoroutineExceptionHandler 接口 ; 并覆盖接口中的 val key 成员变量 和 fun handleException(context: CoroutineContext, exception: Throwable) 成员方法 ;
/**
* 在 MyCoroutineExceptionHandler 全局异常处理器 中处理未捕获异常 ,
但是程序依然崩溃 , 可以在 全局异常处理器 中获取到异常信息 ;
* */
class MyCoroutineExceptionHandler : CoroutineExceptionHandler {
val TAG = "MyCoroutineExceptionHandler"
override val key = CoroutineExceptionHandler
override fun handleException(coroutineContext: CoroutineContext, throwable: Throwable) {
Log.e(TAG, "在MyCoroutineExceptionHandler全局异常处理器 中处理全局未捕获异常 " + (coroutineContext[CoroutineName]?.name ?:"" )
+"\n协程上下文 $coroutineContext" + "\n异常内容 ${throwable.toString()}")
}
}
⑤ 在 app/main/resources/META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler 文件中配置 协程的 全局异常处理器 MyCoroutineExceptionHandler 自定义类 的全路径类名 com.my.runalone_coroutinedemo.MyCoroutineExceptionHandler , 如下图所示 :
⑥ 在 Activity 中实现一个 抛出异常的协程 :
private fun test_MyCoroutineExceptionHandler(){
runBlocking {
val job = GlobalScope.launch(CoroutineName("协程job")) {
Log.e(TAG, "测试协程中抛出异常开始")
throw IllegalArgumentException("job协程空指针异常")
Log.e(TAG, "测试协程中抛出异常完毕")
}
job.join()
}
}
⑦ 执行上述应用 , 协程job会抛出异常 , 协程中也不进行异常处理 , 此时执行结果如下 :
⑧在 MyCoroutineExceptionHandler 全局异常处理器 中处理未捕获异常 , 但是程序依然崩溃 , 可以在 全局异常处理器 中获取到异常信息 ;
异常传播的特殊情况
在上文中介绍到 子协程 运行时 , 产生异常 , 会将异常 传递给 父协程 , 父协程会执行如下操作 :
- ① 取消所有的子协程 : 不仅仅取消产生异常的子协程 , 该父协程下所有的子协程都会取消 ;
- ② 取消父协程 : 将父协程本身取消 ;
- ③ 向父协程的父协程传播异常 : 继续将异常传播给 父协程的父协程
但是也有特殊情况 :
①协程 调用 Job#cancel() 函数 进行取消操作时 , 会 抛出 CancellationException 异常 , 该异常是正常的操作 , 会被忽略 ;
②如果 抛出 CancellationException 异常 取消 子协程 , 其 父协程 不会受其影响 ;
③如果 子协程 抛出的是 其它异常 , 该异常会被传递给 父协程 进行处理 ;
④如果 父协程 有多个子协程 , 多个子协程 都抛出异常 , 父协程会等到 所有子协程 都执行完毕会后 , 再处理 异常 ;
1.取消子协程示例
在下面的代码中 , 在 父协程中 使用 launch 协程构建器 创建了子协程 , 注意 如果想要子协程运行 , 必须在创建完子协程后 调用 yield() 函数 , 让 父协程 让渡线程执行权 , 也就是令 子协程 执行起来 , 否则 主线程 一直占用线程 , 子协程无法执行 ;
子协程执行起来后 , 取消子协程 , 此时 在子协程中 , 会抛出 CancellationException 异常 , 该异常不会传递到 父协程 中 , 父协程 正常执行到结束 ;
private fun yield_test3(){
runBlocking {
// 父协程
val fatherJob = launch {
Log.e(TAG, "父协程fatherJob开始执行")
//子协程
val childJob = launch {
Log.e(TAG, "子协程childJob执行开始")
try {
delay(200)
}finally {
Log.e(TAG, "子协程childJob执行完毕")
}
}
// 让 父协程 让渡线程执行权 , 让 子协程 先执行起来(否则 主线程 一直占用线程 , 子协程无法执行 ;)
yield()
Log.e(TAG, "取消子协程childJob")
// 子协程执行起来后 , 取消子协程 , 此时 在子协程中 , 会抛出 CancellationException 异常 , 该异常不会传递到 父协程 中 , 父协程 正常执行到结束 ;
childJob.cancel()
Log.e(TAG, "父协程fatherJob执行完毕")
}
// 等待父协程执行完毕
fatherJob.join()
}
}
/**
* 父协程fatherJob开始执行
* 子协程childJob执行开始
* 取消子协程childJob
* 父协程fatherJob执行完毕
* 子协程childJob执行完毕
*/
默认 永远是父协程先于子协程执行
private fun yield_test2(){
runBlocking {
// 父协程
val fatherJob = launch {
Log.e(TAG, "父协程fatherJob开始执行")
//子协程
val childJob = launch {
Log.e(TAG, "子协程childJob执行开始")
try {
delay(200)
}finally {
Log.e(TAG, "子协程childJob执行完毕")
}
}
Log.e(TAG, "取消子协程childJob")
childJob.cancel()
Log.e(TAG, "父协程fatherJob执行完毕")
}
// 等待父协程执行完毕
fatherJob.join()
}
}
/**
* 父协程fatherJob开始执行
* 取消子协程childJob
* 父协程fatherJob执行完毕
*/
2. 当子协程中出现异常以后,它们都会统一将异常上报给顶层的父协程,然后顶层的父协程才会去调用 CoroutineExceptionHandler,来处理对应的异常。
父协程 中 使用 launch 创建了 2 个 子协程 ,
子协程 1 执行 2 秒后 , 在 finally 中再执行 1 秒 ;
子协程 2 执行 100 ms 后 , 自动抛出异常 ;
在 子协程 2 抛出异常后 , 两个子协程 都会退出 , 但是 子协程 1 的 finally 代码要执行 1000 ms , 这里父协程 等待 子协程 1 执行完毕后 , 才会处理 子协程 抛出的异常 ;
运行时 子协程 2 会先抛出异常 执行被取消 , 此时 子协程 1 执行也会被取消 父协程 会在 两个子协程都取消后 才会处理异常 执行结果 : 由下面的日志可知 : 子协程 childJob1 没有执行完 2 秒 , 就被 子协程 childJob2 的异常打断了 , 但是 子协程 childJob1 中的 finally 代码中的 1 秒执行完毕了 ; 子协程 childJob2 早早抛出异常退出了 , 子协程 1 还执行了 1 秒 , 最后 父协程 等 子协程 childJob1 执行完毕后 , 才处理的 子协程 2 抛出的 异常 ;
private fun test_MyCoroutineExceptionHandler3(){
runBlocking {
// 父协程
val fartherJob = GlobalScope.launch(CoroutineName("父协程fartherJob")) {
Log.e(TAG, "父协程fartherJob开始执行")
// 子协程 1
val childJob1 =launch{
Log.e(TAG, "子协程 childJob1 执行开始")
try {
delay(2000)
}finally{
withContext(NonCancellable){
delay(1000)
Log.e(TAG, "子协程 childJob1 执行完毕")
}
}
}
// 子协程 2
val childJob2 = launch {
Log.e(TAG, "子协程 childJob2 执行开始")
delay(100)
Log.e(TAG, "子协程 childJob2 抛出 IllegalArgumentException 异常")
throw IllegalArgumentException("子协程 childJob2空指针异常")
Log.e(TAG, "子协程 childJob2 执行完毕")
}
}
// 等待父协程执行完毕
fartherJob.join()
Log.e(TAG, "父协程fartherJob执行完毕")
}
}
/** 运行时 子协程 2 会先抛出异常 执行被取消 , 此时 子协程 1 执行也会被取消
* 父协程 会在 两个子协程都取消后 才会处理异常
*
*
* 执行结果 : 由下面的日志可知 :
* 子协程 childJob1 没有执行完 2 秒 , 就被 子协程 childJob2 的异常打断了 ,
* 但是 子协程 childJob1 中的 finally 代码中的 1 秒执行完毕了 ;
* 子协程 childJob2 早早抛出异常退出了 , 子协程 1 还执行了 1 秒 ,
* 最后 父协程 等 子协程 childJob1 执行完毕后 , 才处理的 子协程 2 抛出的 异常 ;
*
* 父协程fartherJob开始执行
* 子协程 childJob1 执行开始
* 子协程 childJob2 执行开始
* 子协程 childJob2 抛出 IllegalArgumentException 异常
* 子协程 childJob1 执行完毕
*
* 在MyCoroutineExceptionHandler全局异常处理器 中处理全局未捕获异常 父协程fartherJob, 协程上下文 [CoroutineName(父协程fartherJob), StandaloneCoroutine{Cancelling}@f505e0f, Dispatchers.Default], 异常内容 java.lang.IllegalArgumentException: 子协程 childJob2空指针异常
* */
异常聚合 ( 多个子协程抛出的异常会聚合到第一个异常中 )
父协程 中 有多个 子协程并列执行 , 这些子协程 都 抛出了 异常 , 此时 只会处理 第一个 异常 ;
这是因为 多个 子协程 , 如果出现了多个异常 , 从第二个异常开始 , 都会将异常绑定到第一个异常上面 ;
在 CoroutineExceptionHandler 中 , 调用
throwable.suppressed.contentToString()
可以获取多个异常 , 被绑定的异常会存放到一个数组中 , 有多少个异常都会显示出来 ;
运行时 子协程 2 会先抛出异常 , 此时 子协程 1 也会被取消 , 在 finally 中抛出异常
父协程 会在 两个协程都取消后 才会处理异常
第二个异常 会被 绑定到 第一个异常 上
private fun many_childCoroutine_ExceptionHandler(){
runBlocking {
// 父协程
val fartherJob = GlobalScope.launch(CoroutineName("父协程fartherJob")) {
Log.e(TAG, "父协程fartherJob开始执行")
// 子协程 1
val childJob1 =launch{
Log.e(TAG, "子协程 childJob1 执行开始")
try {
delay(2000)
}finally{
Log.e(TAG, "子协程 childJob1 抛出 IllegalArgumentException 异常 ( 第二个异常 )")
throw IllegalArgumentException("子协程 childJob1 抛出 IllegalArgumentException 异常")
}
}
// 子协程 2
val childJob2 = launch {
Log.e(TAG, "子协程 childJob2 执行开始")
delay(100)
Log.e(TAG, "子协程 childJob2 抛出 ArithmeticException 异常 ( 第一个异常 )")
throw ArithmeticException("子协程 childJob2 抛出 ArithmeticException 异常")
Log.e(TAG, "子协程 childJob2 执行完毕")
}
}
// 等待父协程执行完毕
fartherJob.join()
Log.e(TAG, "父协程fartherJob执行完毕")
}
}
/** 运行时 子协程 2 会先抛出异常 , 此时 子协程 1 也会被取消 , 在 finally 中抛出异常
* 父协程 会在 两个协程都取消后 才会处理异常
* 第二个异常 会被 绑定到 第一个异常 上
*
*
* 父协程fartherJob开始执行
* 子协程 childJob1 执行开始
* 子协程 childJob2 执行开始
* 子协程 childJob2 抛出 ArithmeticException 异常 ( 第一个异常 )
* 子协程 childJob1 抛出 IllegalArgumentException 异常 ( 第二个异常 )
*
* 在我的自定义全局异常处理器MyCoroutineExceptionHandler中捕获全局异常 父协程fartherJob
* 协程上下文: [CoroutineName(父协程fartherJob), StandaloneCoroutine{Cancelling}@7b38e23, Dispatchers.Default]
* 捕获全局异常信息: [java.lang.IllegalArgumentException: 子协程 childJob1 抛出 IllegalArgumentException 异常]
*
* */
协程取消异常(CancellationException)小结:
-
第一条准则:协程的取消需要内部的配合。
-
第二条准则:不要轻易打破协程的父子结构!这一点,其实不仅仅只是针对协程的取消异常,而是要贯穿于整个协程的使用过程中。我们知道,协程的优势在于结构化并发,它的许多特性都是建立在这个特性之上的,如果我们无意中打破了它的父子结构,就会导致协程无法按照预期执行。
-
第三条准则:捕获了 CancellationException 以后,要考虑是否应该重新抛出来。在协程体内部,协程是依赖于 CancellationException 来实现结构化取消的,有的时候我们出于某些目的需要捕获 CancellationException,但捕获完以后,我们还需要思考是否需要将其重新抛出来。
-
第四条准则:不要用 try-catch 直接包裹 launch、async。协程代码的执行顺序与普通程序不一样,我们直接使用 try-catch 包裹 launch、async,是不会有任何效果的。
-
第五条准则:灵活使用 SupervisorJob,控制异常传播的范围。SupervisorJob 是一种特殊的 Job,它可以控制异常的传播范围。普通的 Job,它会因为子协程中的异常而取消自身,而 SupervisorJob 则不会受到子协程异常的影响。在很多业务场景下,我们都不希望子协程影响到父协程,所以 SupervisorJob 的应用范围也非常广。比如说 Android 中的 viewModelScope,它就使用了 SupervisorJob,这样一来,我们的 App 就不会因为某个子协程的异常导致整个应用的功能出现紊乱。
-
第六条准则:使用 CoroutineExceptionHandler 处理复杂结构的协程异常,它仅在顶层协程中起作用。传统的 try-catch 在协程中并不能解决所有问题,尤其是在协程嵌套层级较深的情况下。这时候,使用 CoroutineExceptionHandler 就可以轻松捕获整个作用域内的所有异常。
当我们遇到问题的时候,首先要分析是 CancellationException 导致的,还是其他异常导致的。接着我们就可以根据实际情况去思考,该用哪种处理手段了。
其实上面这 6 大准则,都跟协程的结构化并发有着密切联系。由于协程之间存在父子关系,因此它的异常处理也是遵循这一规律的。而协程的异常处理机制之所以这么复杂,也是因为它的结构化并发特性。
鸣谢:
https://blog.csdn.net/shulianghan/category_12116559.html
https://blog.csdn.net/shulianghan/category_12116559_2.html
Android中对Kotlin Coroutines(协程)的理解(一) - 简书
https://www.cnblogs.com/baiqiantao/p/6378850.html