本文主要是介绍二次重温协程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
术语解释
CoroutineExceptionHandler
CoroutineContext 的一部分,用于处理未捕获的异常。应在根协程中使用,以处理所有子协程的未捕获异常。
协程概念(Coroutine)
协程就像轻量级的线程,协程是依赖于线程,一个线程中可以创建N个协程,很重要的一点就是协程挂起时不会阻塞线程.
协程提供了一种避免阻塞线程并用更简单、更可控的操作替代线程阻塞的方法:协程挂起和恢复。
本质上Kotlin协程就是作为在Kotlin语言上进行异步编程的解决方案,处理异步代码的方法。
协程可以使用阻塞的方式写出非阻塞式的代码,解决并发中常见的回调地狱。消除了并发任务之间的协作的难度,协程可以让我们轻松地写出复杂的并发代码。一些本来不可能实现的并发任务变的可能,甚至简单,这些才是协程的优势所在。
协程 是 基于 线程 的 , 是 轻量级 线程 ;
在实际线程框架之上编写的用于管理并发的框架。最简单的定义是,可以在不阻塞线程的情况下挂起和恢复的代码片段。
协程的概念最核心的点就是一个函数或者一段程序能够被协程挂起,稍后再在挂起的位置恢复。
协程通过主动让出运行权来实现协作,程序自己处理挂起和恢复来实现程序执行流程的协作调度。因此它本质上就是在讨论程序控制流程的机制。
协程是一种可以挂起和恢复执行的轻量级线程。协程可以让我们以同步的方式编写异步代码,使得代码更加简洁易读。在Kotlin中,我们可以使用launch
或async
函数来创建并启动一个协程。
协程的作用
- 处理耗时任务 : 耗时任务 通常需要 阻塞主线程 , 线程量级太重 , 耗时任务 推荐在协程中执行 ;
- 保证主线程安全 : 从主线程中 安全地调用可能会挂起的函数 ;
异步任务 AsyncTask 也可以处理耗时操作 , 避免耗时任务阻塞线程 , 但是在 Android 11 中 , 官方规定 该 api 已过时 , 被标记为弃用状态 , 建议使用
协程
java.util.concurrent 包下的 Executor,ThreadPoolExecutor,FutureTask取代 AsyncTask
协程在Android上的主要使用场景:
- 1、线程切换,保证线程安全。
- 2、处理耗时任务(比如网络请求、解析
JSON
数据、从数据库中进行读写操作等)。
Kotlin为什么要使用协程
我们使用 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 类型 值 33val result :User = mainViewModel.getUserSuspend()result.id}// 调用协程体的 createCoroutine 函数 , 传入 Continuation 实例对象 , 一般传入一个 对象表达式 object : Continuation<Int>//Continuation 后的 <Int> 泛型 表示的是协程体的返回值类型Int ;.createCoroutine(object:Continuation<Int>{// 协程上下文设置为 空的协程上下文 EmptyCoroutineContextoverride 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"
}
进程 线程(主,IO) 协程的关系
协程Coroutine不能脱离线程(主,IO)而运行,但可以在不同的线程(主,IO)之间切换,而且一个线程(主,IO)上可以一个或多个不同的协程。
协程的构建(创建/构造器)CoroutineBuilder
负责创建和启动新协程的函数。例如,launch{}、async{}、runBlocking{}。
协程 需要 协程构建器 来启动 , 协程构建器launch{}、async{} 就是 CoroutineScope 协程作用域的两个扩展函数 ;
launch:Job
构建器
launch 构建器 : 返回 Job 实例对象 , 但是该协程任务没有返回值;
(注意:与async:Deferred<T>的区别
)
launch 函数是 CoroutineScope 协程作用域 类的扩展函数 ;
创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。它返回的是一个该协程任务的引用,即Job
对象。这是最常用的用于启动协程的方式。
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) elseStandaloneCoroutine(newContext, active = true)coroutine.start(start, coroutine, block)return coroutine
}
launch
函数是一种协程构建器,它用于创建并启动一个新的协程。launch
函数返回一个Job
对象,我们可以使用这个对象来管理协程的生命周期。
val job = GlobalScope.launch {doSomething()
}
job.cancel() // 取消协程
async:Deferred<T>协程构建器
也是一种协程构建器,它用于创建并启动一个新的协程。
与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) elseDeferredCoroutine<T>(newContext, active = true)coroutine.start(start, coroutine, block)return coroutine
}
案例:
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 msdelay(500)Log.e(TAG,"launchJob1 执行")"方明飞1" //这个值不会返回}Log.e(TAG,launchJob1.toString() ) //todo 协程没有返回结果值 StandaloneCoroutine{Active}@2546022val launchJob2:Job =this@runBlocking.launch{delay(400)Log.e(TAG,"launchJob2 执行")"方明飞2"}Log.e(TAG,launchJob2.toString() ) //todo 协程没有返回结果值 StandaloneCoroutine{Active}@f837ab3val asyncJob3 : Deferred<Any> = this@runBlocking.async{// 调用该挂起函数延迟 100 msdelay(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 执行}
launch 协程执行顺序控制--Job#join() 函数
如果需要通过 launch 协程构建器 启动多个并行子协程 , 后面的子协程需要等待前面的子协程执行完毕 , 在启动靠后的协程 , 实现方案如下 :
调用 Job#join() 函数 , 可以挂起父协程 , 等待前面的launch中子协程体内的任务执行完毕 , 再执行后面的子协程任务 ;
Job#join()函数是挂起函数 , 不会阻塞主线程 ;
/*** 挂起协程,直到此作业完成。此调用正常恢复(没有异常)* 当作业因任何原因完成且调用协程的[job]仍为[active][isActive]时。* 这个函数也[启动][Job。如果[Job]仍然处于_new_状态,则启动]相应的协程。** 注意,只有当所有子任务都完成时,作业才算完成。** 这个挂起函数是可取消的,并且**总是**检查是否取消了调用协程的Job。* 如果调用协程的[Job]被取消或完成* 函数被调用,或当它被挂起时,此函数* 把[CancellationException]。** 特别是,它意味着父协程在子协程上调用' join '时抛出* [CancellationException]如果子进程失败,因为子进程的失败会默认取消父进程,* 除非子进程是从[supervisor orscope]内部启动的。** 此函数可用于带有[onJoin]子句的[select]调用。* 使用[isCompleted]检查该作业是否已完成,无需等待。** 有一个[cancelAndJoin]函数,它结合了[cancel]和' join '的调用。*/public suspend fun join()
async 协程执行顺序控制--Deferred#await() 函数
如果需要通过 async 协程构建器 启动多个并行子协程 , 后面的子协程需要等待前面的子协程执行完毕 , 在启动靠后的子协程 , 实现方案如下 :
调用 Deferred#await() 函数 , 可以挂起父协程 , 等待 async 中子协程体内的任务执行完毕 , 再执行后面的协程任务 ;
Deferred#await() 函数是挂起函数 , 不会阻塞主线程 ;
/*** 在不阻塞线程的情况下等待该值的完成,并在延迟的计算完成时恢复,* 返回结果值,如果取消了延迟,则抛出相应的异常。** 这个暂停功能是可以取消的。* 如果当前协程的[Job]在此挂起函数等待时被取消或完成,则此函数* 立即恢复[CancellationException]。* 有**立即取消的保证**。如果在此函数被取消时作业被取消* 挂起后,它将无法成功恢复。有关底层细节,请参阅[suspendCancellableCoroutine]文档。** 这个函数可以在[select]调用和[onAwait]子句中使用。* 使用[isCompleted]检查这个延迟值是否已经完成,无需等待。*/
public suspend fun await(): T
案例:
private fun runBlockingTest3() {// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程runBlocking(block={// 在 runBlocking 代码块中 , 可以 直接调用 CoroutineScope 的扩展方法 , 如 launch , async 函数 ;val launchJob1:Job = this@runBlocking.launch{// // 调用该挂起函数延迟 500 msdelay(500)Log.e(TAG,"launchJob1 执行")"方明飞1" //这个值不会返回}Log.e(TAG,launchJob1.toString() ) //todo 协程没有返回结果值 StandaloneCoroutine{Active}@2546022// todo 挂起父协程 , 等待launchJob1子协程执行完毕会后再执行后面的其他子协程任务launchJob1.join() //要等子协程launchJob1执行完后,才会执行后面的子协程job2 job3val launchJob2:Job =this@runBlocking.launch{// 调用该挂起函数延迟 400 msdelay(400)Log.e(TAG,"launchJob2 执行")"方明飞2"}Log.e(TAG,launchJob2.toString() ) //todo 协程没有返回结果值 StandaloneCoroutine{Active}@f837ab3// todo 挂起父协程 , 等待launchJob2子协程执行完毕会后再执行后面的其他子协程任务launchJob2.join() //要等子协程launchJob2执行完后,才会执行后面的子协程job2 job3val asyncJob3 : Deferred<String> = this@runBlocking.async{// // 调用该挂起函数延迟 300 msdelay(300)Log.e(TAG,"asyncJob3 执行")"方明飞3"}//挂起父协程 , 等待asyncJob3子协程执行完毕会后再执行后面的子协程任务 获取 asyncJob3 协程返回值val asyncJob3_result = asyncJob3.await()Log.e(TAG, "输出 asyncJob3 协程返回值 : ${asyncJob3_result}") //todo 输出 asyncJob3 协程返回值 : 方明飞3val asyncJob4 : Deferred<Int> = this@runBlocking.async{// // 调用该挂起函数延迟 200 msdelay(200)Log.e(TAG,"asyncJob4 执行")1987}//挂起父协程 , 等待asyncJob4子协程执行完毕会后再执行后面的子协程任务 获取 asyncJob4 协程返回值val asyncJob4_result = asyncJob4.await()Log.e(TAG, "输出 asyncJob4 协程返回值 : ${asyncJob4_result}") //todo 输出 asyncJob3 协程返回值 : 1987val asyncJob5 : Deferred<Boolean> = this@runBlocking.async{// // 调用该挂起函数延迟 100 msdelay(100)Log.e(TAG,"asyncJob5 执行")true}Log.e(TAG, "输出 asyncJob5 协程返回值 : ${asyncJob5.await()}") //todo 输出 asyncJob5 协程返回值 : true})/**todo 输出各子协程内容的顺序:launchJob1 执行launchJob2 执行asyncJob3 执行输出 asyncJob3 协程返回值 : 方明飞3asyncJob4 执行输出 asyncJob4 协程返回值 : 1987asyncJob5 执行输出 asyncJob5 协程返回值 : true*/}
什么是Job
Job
是协程的句柄。如果把门和门把手比作协程和Job
之间的关系,那么协程就是这扇门,Job
就是门把手。意思就是可以通过Job
实现对协程的控制和管理。
Job
我们可以认为他就是一个协程作业是通过CoroutineScope.launch
生成的,同时它运行一个指定的代码块,并在该代码块完成时完成。我们可以通过isActive
、isCompleted
、isCancelled
来获取到Job
的当前状态。
什么是Deferred
Deferred 继承了 Job 接口 , 是 Job 接口的子接口 ;
public interface Deferred<out T> : Job {public suspend fun await(): Tpublic val onAwait: SelectClause1<T>@ExperimentalCoroutinesApipublic fun getCompleted(): T@ExperimentalCoroutinesApipublic fun getCompletionExceptionOrNull(): Throwable?
}
协程的生命周期
协程生命周期状态
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 状态 ;
协程任务泄漏
协程任务泄漏 : 发起 协程任务 后 , 无法追踪任务的执行结果 , 任务等于无效任务 , 但是仍然会消耗 内存 , CPU , 网络 , 磁盘 等资源 ;
Kotlin 中引入了 结构化并发机制 避免 协程任务泄漏 的情况发生 ;
协程任务泄漏 与 内存泄漏 类似 ;
协程结构化并发
结构化并发 使用场景 :
- 协程任务取消 : 在不需要协程任务的时候 , 取消协程任务 ;
- 追踪协程任务 : 追踪正在执行的协程任务 ;
- 发出错误信号 : 如果 协程任务执行失败 , 发出错误信号 , 表明执行任务出错 ;
协程任务 运行时 , 必须指定其 CoroutineScope 协程作用域 , 其会追踪所有的 协程任务 , CoroutineScope 协程作用域 可以取消 所有由其启动的协程任务 ;
作用域(Scope):
在协程中,作用域定义了协程的生命周期和上下文。每个协程都在某个作用域内运行。
协程作用域 Coroutine Scope
协程作用域定义了协程的生命周期。在Kotlin中,我们可以使用CoroutineScope
接口或coroutineScope
和supervisorScope
函数来定义协程作用域。协程作用域可以确保在作用域内启动的所有协程在作用域结束时都被取消。
协程作用域(Coroutine Scope
)是协程运行的作用范围。为了确保所有的协程都会被追踪到,Kotlin 不允许在没有使用CoroutineScope
的情况下启动新的协程。它能启动新的协程,同时这个协程还具备上面所说的suspend
和resume
的优势。
CoroutineScope
定义了新启动的协程作用范围,同时会继承了他的coroutineContext
自动传播其所有的 elements
和取消操作。换句话说,如果这个作用域销毁了,那么里面的协程也随之失效。
因为启动协程需要作用域,但是作用域又是在协程创建过程中产生的,这似乎是一个“先有鸡后有蛋还是先有蛋后有鸡”的问题。
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)
协程作用域的子协程(Child Coroutine)
在一个协程作用域内启动的协程被称为子协程。子协程的生命周期受其父协程作用域的约束,当父协程作用域结束时,所有子协程都会被取消。
1.常见的 CoroutineScope 协程作用域
runBlocking
函数: T
顶层函数,创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,返回值是泛型T(
就是你协程体中最后一行是什么类型,最终返回的是什么类型T
就是什么类型。)
,一般在项目中不会使用,主要是为main函数和测试设计的。
runBlocking 是 常规普通函数 , 可以在 普通的代码位置使用 , 将 主线程 或 子线程 包装成 协程体 , 在该协程中执行 协程任务 , 会 阻塞当前的线程 ;
在 runBlocking 代码块中 , 可以 直接调用 CoroutineScope 的扩展方法 , 如 launch , async 函数 ;
runBlocking {// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程
}
public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T
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} ") //maincoroutineScope {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} ") //maincoroutineScope {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 协程执行了 mainjob1 协程开始执行job2 协程开始执行job3 协程开始执行job4 协程开始执行job1 协程执行完毕job4 协程 抛出异常取消执行*/
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) elseStandaloneCoroutine(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
):
该作用域仅在 Activity 中使用 , 与 Activity 生命周期绑定 ;
Lifecycle Ktx
库提供的具有生命周期感知的协程作用域,与Lifecycle
绑定生命周期,生命周期被销毁时,此作用域将被取消。会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,推荐使用。
因为Activity
实现了LifecycleOwner
这个接口,而lifecycleScope协程
则正是LifecycleOwner
的拓展成员,可以在Activity中可以直接使用lifecycleScope
协程实例:
package androidx.lifecycle
import kotlinx.coroutines.CoroutineScope
public interface LifecycleOwner {public val lifecycle: Lifecycle
}public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScopeget() = lifecycle.coroutineScope
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)coroutineScope 与 CoroutineScope的区别
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 deferred2delay(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 {@JvmStaticpublic actual val Default: CoroutineDispatcher = createDefaultDispatcher()@JvmStaticpublic actual val Main: MainCoroutineDispatcherget() = MainDispatcherLoader.dispatcher@JvmStaticpublic actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined@JvmStaticpublic val IO: CoroutineDispatcher = DefaultScheduler.IO
}
调度器模式 | 说明 | 适用场景 | 注意 |
| 默认调度器,在 子线程 中运行 , 处理 CPU 耗时任务 , 主要侧重算法消耗 ; | 通常处理一些单纯的计算任务,或者执行时间较短任务比如:数据排序 , 数据解析 , 数据对比 等耗时算法操作 ,Json 的解析,数据计算等。 | 在子线程 中执行耗时任务 |
| UI 调度器, Andorid 上的主线程。处理 UI 交互任务 | 调用 挂起 suspend 函数 , 更新 UI , 更新 LiveData ; | 在协程中调用 挂起 suspend 函数 , 必须在 Dispatchers.Main 调度器中执行 ; |
| 一个不局限于任何特定线程的协程调度程序,即非受限调度器。 | 子协程切换线程代码会运行在原来的线程上,协程在相应的挂起函数使用的任何线程中继续。 | |
| 在 子线程 中运行 , 处理 文件操作 和 网络 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")}}}
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 {......
}
模版:
我们可以先使用 launch(Dispatchers.Main)
启动一个协程(默认在主线程),再通过withContext(Dispatchers.IO)
调度到IO
线程上去做网络请求,把得到的结果返回,当前协程拿到数据进行UI更新
GlobalScope.launch(Dispatchers.Main) {val result = withContext(Dispatchers.IO) {//网络请求..."请求结果"}btn.text = result
}
案例:
在主线程 创建一个协程GlobalScope , 通过withContext(Dispatchers.IO)调度(切换)到IO线程上去做网络请求 ,主线程上的协程GlobalScope此时就会恢复继续执行,完成UI的更新
private fun withContextTest() {// 在主线程 创建一个协程GlobalScopeGlobalScope.launch(Dispatchers.Main) {// 通过withContext(Dispatchers.IO)调度(切换)到IO线程上去做网络请求val result: User = withContext(context=Dispatchers.IO,block= { //切换到IO线程 网络请求mainViewModel.getUserSuspend()})// 主线程上的协程GlobalScope此时就会恢复继续执行,完成UI的更新tv_title.text=result.name}}
协程上下文CoroutineContext:
为协程提供上下文信息(CoroutineDispatcher、Job、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 {//这个协程上下文元素的keypublic 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
集合。
get()
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
public interface Job : CoroutineContext.Element {public companion object Key : CoroutineContext.Key<Job> {//省略...}
}
CoroutineDispatcher
public abstract class CoroutineDispatcher :AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(ContinuationInterceptor,{ it as? CoroutineDispatcher })
}
CoroutineExceptionHandler
public interface CoroutineExceptionHandler : CoroutineContext.Element {public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
}
ContinuationInterceptor
public interface ContinuationInterceptor : CoroutineContext.Element {companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}
CoroutineName
CoroutineName
是用户用来指定的协程名称的,用于方便调试和定位问题:
//用户指定的协程名称。此名称用于调试模式。
public data class CoroutineName(//定义协程的名字val name: String
) : AbstractCoroutineContextElement(CoroutineName) {//CoroutineName实例在协程上下文中的keypublic 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.MainLog.e(TAG, "当前协程的上下文元素=$context")// 当前协程的上下文元素=[CoroutineName(上下文1), Dispatchers.Main]context += Dispatchers.IO + CoroutineName("上下文2")//添加重复Dispatchers元素,Dispatchers.IO 会替换 ispatchers.MainLog.e(TAG, "当前协程的上下文元素=$context")
// 当前协程的上下文元素=[CoroutineName(上下文2), Dispatchers.IO]context = context.minusKey(context[CoroutineName]!!.key) //移除CoroutineName元素Log.e(TAG, "当前协程的上下文元素=$context")
// 当前协程的上下文元素=Dispatchers.IO}}
协程启动模式
CoroutineStart.DEFAULT 启动模式
默认启动模式,饿汉启动模式, 协程创建后,立即开始调度执行(没有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
的start
、join
或者await
等函数时才会开始调度执行。
在下面的代码中 , 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
启动模式
协程创建后。马上开始调度执行 。
通过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
启动模式
协程创建后 , 立即在当前的 函数调用栈(主线程) 执行协程任务 , 直到遇到第一个挂起函数 , 才在子线程中执行挂起函数 ;
如果在主线程中启动协程 , 则该模式的协程就会直接在主线程中执行 ;
如果在子线程中启动协程 , 则该模式的协程就会直接在子线程中执行 ;
案例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.coroutinesimport 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 stabilityATOMIC,/*** 立即执行协程,直到它在当前线程中的第一个挂起点_* 正在使用[dispatchers . unrestricted]启动协程。但是,当从挂起恢复协程时* 它根据上下文中的[CoroutineDispatcher]进行分派。** 这与[ATOMIC]在某种意义上类似,协程开始执行,即使它已经被取消,* 但不同的是,它在同一个线程中开始执行。** 协程在挂起点上的可取消性取决于的具体实现细节* suspend功能如[DEFAULT]。** 无限制事件循环** 与调度程序。和[MainCoroutineDispatcher。],嵌套的未分派协程不会形成* 在无限制嵌套的情况下防止潜在堆栈溢出的事件循环。*/UNDISPATCHED;/*** 当[LAZY]时返回' true '。** @suppress **这是一个内部API,不应该从通用代码中使用*/@InternalCoroutinesApipublic val isLazy: Boolean get() = this === LAZY
}
协程的挂起和恢复
1.函数 最基本的操作 是 :
- 调用 call : 通过 函数名或函数地址 调用函数 ;
- 返回 return : 函数执行完毕后 , 继续执行函数调用的下一行代码 ;
2.协程 在 调用 call 和 返回 return 基础上 , 又新增了两种 状态 :
- 挂起 Suspend : 暂停当前执行的协程 , 保存挂起点的局部变量 , 然后执行异步任务 , 后面的代码会等到异步任务执行完毕后 , 恢复 Resume 挂起状态后再执行后续代码 ;
- 恢复 Resume : 暂停的协程 继续执行 ;
如果 没有挂起Suspend 操作 , 在子线程中执行异步任务时 , 会马上执行后续的代码 (不会等待异步任务), 只是相当于 普通的多线程操作并发执行(同时各执行各的,这样就没有意义了) ;
协程的作用就是 可以 顺序地执行 异步任务 和 主线程任务 , 其执行顺序按照代码顺序执行 ;
(执行异步耗时任务时,主协程被挂起(后面的代码不能执行),异步耗时任务执行完毕后,恢复主协程,执行后面的代码)
3. 协程的 suspend 挂起函数
挂起suspend 函数 , 只能在 协程体内部 或者 其它挂起函数(带有suspend ) 中调用 ;协程外部不允许使用挂起函数 ;
在协程中 , 执行 挂起 Suspend 函数 ,把父协程给挂起来, 将 挂起点的信息 记录下来 , 然后执行耗时异步操作任务 ,耗时异步操作任务 执行完毕后 恢复 Resume父协程 ,在继续执行父协程后面的代码;
声明挂起函数 , 使用 suspend
在 fun
关键字之前 修饰函数 ,如果在 函数 A 中调用上述 Test 挂起函数 , 则 函数 A 也必须是 挂起函数 ;
在协程中 , GlobalScope.launch(Dispatcher.Main){}
中 , 可以直接调用/使用挂起函数 ;
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?=nullprivate suspend fun asynTask() {// 子线程中执行异步任务result = mainViewModel.getUserSuspend()}private fun updateMain() {// 主线程更新 UItv_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 函数 )
协程挂起 和 线程阻塞 对比
协程挂起
挂起是协程中的概念 , 只能在协程中使用 ;
协程 挂起 操作 : 在协程中使用 suspend delay() 函数 , 挂起 20 秒时间 , 然后 20 秒后更新 UI ; delay 函数是 挂起 suspend 函数 ;
案例:见上
线程阻塞
阻塞是线程中的概念 , 可以在主线程和子线程中使用 ;
主线程 阻塞 操作 : 在主线程 中使用 Thread.sleep 函数 , 阻塞 20 秒时间 , 然后 20 秒后更新 UI ;
案例:
// 主线程阻塞
Thread.sleep(20000)
// 主线程更新 UI
Log.i("MainActivity", "GlobalScope : 主线程更新 UI")
挂起和阻塞对 UI 的影响
1.协程 挂起 操作 不会出现 阻塞 UI 刷新的情况 , 挂起的 10/20 秒不影响 UI 刷新显示 ;协程中有挂起操作 , 会将挂起点的状态保存 , 同时协程停止执行 , 等待挂起函数执行完毕后 , 协程继续执行 ; 相当于阻塞的是协程 , 不会阻塞主线程 ;
2.但是如果将主线程阻塞 , UI 不再刷新 , 会出现 ANR 崩溃异常 ;图形化 GUI 系统中 , 一般都在主线程中更新 UI , 主线程中都有一个无限循环 , 不断刷新界面 , 如果在主线程中执行了耗时操作 , 就会影响到界面的刷新 , 出现漏帧 , ANR 崩溃异常 ;
挂起函数串行执行
在协程体中 , 连续使用多个挂起函数 , 这些挂起函数的执行是顺序执行的 , 挂起函数 1 执行完毕后 , 才执行 挂起函数 2 ;
案例:
private fun suspendSerialExecution() {runBlocking {// 调用 runBlocking 函数 , 可以将 主线程 包装成 协程// measureTimeMillis 函数用于测量内部代码块执行的时间, 单位毫秒 msval 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 函数用于测量内部代码块执行的时间, 单位毫秒 msval 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 , 达到了并发执行减少执行时间的目的 ;
挂起函数(Suspend Function):
挂起函数是一种可以在不阻塞线程的情况下挂起和恢复执行的函数。在Kotlin中,我们可以使用suspend
关键字来定义一个挂起函数。挂起函数只能在协程或其他挂起函数中调用。
suspend fun doSomething() {delay(1000L)println("Hello, Suspend Function!")
}
协程取消
协程取消 :
取消协程作用域 : 取消 协程作用域 会将该作用域中的 所有 子协程 一同取消 ;
取消子协程 : 子协程 的取消 不会影响 同一层级的 兄弟协程的执行 ;
通过抛出异常取消协程 : 协程取消通常会通过 抛出 CancellationException 异常 实现 ;
挂起函数取消 : 定义在 kotlinx.coroutines 包下的 suspend 挂起函数 是可以取消的 , 如 delay 函数
【协程作用域】取消
案例1:协程作用域 不取消 会等所有的子协程执行完毕
private fun cancelcoroutineScope() {// 创建协程作用域val job0 = CoroutineScope(Dispatchers.Default)//可以创建一个子协程.launch {Log.e(TAG, "job0 子协程执行开始")delay(2000)Log.e(TAG, "job0 子协程执行完毕")}}/**协程作用域 不取消 会等所有的子协程执行完毕* job0 子协程执行开始* job0 子协程执行完毕* */
案例2:取消 该协程作用域之后 , 该作用域下的 所有子协程都被取消了 , 子协程都不会执行完毕
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"}val async2: Deferred<String> = coroutineScope.async{Log.e(TAG, "async2 子协程执行开始")delay(2000)Log.e(TAG, "async2 子协程执行完毕")"456"}// 100ms 后取消协程作用域delay(100)// 取消协程作用域coroutineScope.cancel()Log.e(TAG, "输出 async1 子协程的结果值 ${async1.await()}") //这里无法输出 取消 coroutineScope 协程作用域之后 , 该作用域下的 async1 子协程 被取消了 不会执行Log.e(TAG, "输出 async2 子协程的结果值 ${async2.await()}") //这里无法输出 取消 coroutineScope 协程作用域之后 , 该作用域下的 async2 子协程 被取消了 不会执行}}/***取消 coroutineScope 协程作用域之后 , 该作用域下的 async1 和 async2 子协程都被取消了 , 两个子协程都没有执行完毕 ;* async1 子协程执行开始*async2 子协程执行开始** */
案例3:到此 明天搞
鸣谢:
https://blog.csdn.net/shulianghan/category_12116559.html
https://blog.csdn.net/shulianghan/category_12116559_2.html
Android中对Kotlin Coroutines(协程)的理解(一) - 简书
这篇关于二次重温协程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!