原文: Kotlin 协程基础使用学习-Stars-One的杂货小窝
本篇阅读可能需要以下知识,否则可能阅读会有些困难
- 客户端开发基础(Android开发或JavaFx开发)
- Java多线程基础
- kotlin基础
本文尽量以使用为主,以代码为辅讲解,不提及过深协程底层代码逻辑,仅做一个基础入门来快速上手学习(断断续续写了好几个周,若是有错误之处也请在评论区提出 ?)
协程优点
首先,先说下为什么使用协程吧
协程得和线程进行比较
- 可在单个线程运行多个协程,其支持挂起,不会使运行协程的线程阻塞。
- 协程可以取消
- 协程可以让异步代码同步化,其本质是轻量级线程,进而可以降低异步程序的设计复杂度。
对于客户端的网络请求数据,以往写法都是在回调操作里进行更新UI操作,一旦业务复杂,且需要调用多个接口
如接口A调用完后得到的数据A需要进行拼接,从而构造成接口B的参数,去请求接口B得到数据,那么就得在里面疯狂套娃,难管理且阅读很难受
而采用协程,则将异步操作变为同步操作,如下图:
需要注意的是,并不是说协程比线程池进行并发任务性能更好,实际上协程内部还是使用线程调度的那一套,只不过对于开发者来说,更是黑箱操作
只是对于客户端开发来说,可以从那种回调处理更新UI的解放出来
从性能上去看:
协程的性能并不优于线程池或者其他异步框架,主要是其做了更多语言级别步骤,但通常情况下,与其他框架的性能几乎一致,因为相比IO的耗时,语言级别的损耗可以几乎忽略不计;
从设计模式去看:
协程使得开发者可以自行管理异步任务,而不同于线程的抢占式任务,并且写成还支持子协程的嵌套关闭、更简便的异常处理机制等,故相比其他异步框架,协程的理念更加先进;
入门使用
依赖说明
kotlin的协程是一个单独的库,需要我们进行依赖后才能使用
这里需要说明一下,协程分为了几个Module,需要根据情况引用(我这里只介绍其中几个常用的模块,需要了解更多可以去看官方文档说明)
- kotlinx-coroutines-core
- kotlinx-coroutines-core-jvm
- kotlinx-coroutines-android
- kotlinx-coroutines-javafx
kotlinx-coroutines-core
模块是针对多平台项目一个公共库,Kotlin/Native、Kotlin/JVM 和 Kotlin/JS 上使用。
kotlinx-coroutines-core-jvm
是专门为在 JVM 平台上运行的项目设计,并提供了一些额外的功能,比如提供针对 JVM 的调度器和扩展函数。
而kotlinx-coroutines-android
和kotlinx-coroutines-javafx
则是针对的特定的UI平台,提供了对应的调度器,Android是Dispatcher.Main
,JavaFx则是Dispatch.JavaFx
(实际上也能用Dispatch.Main
,与Dispatch.JavaFx
等同的)
PS: 这里如果不懂Dispatchers,没有关系,只需要记住这个就是方便我们切换到UI线程(主线程)操作即可
像我一般是在Android平台或者是JavaFx平台,没有JS和Native的需求
所以一般引用kotlinx-coroutines-core-jvm
即可,会自动将kotlinx-coroutines-core
也引入
之后根据平台选择kotlinx-coroutines-android
或kotlinx-coroutines-javafx
依赖
引入依赖(示例):
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core-jvm</artifactId>
<version>1.8.0-RC2</version>
</dependency>
//这里省略了对应平台的版本依赖,参考下面gradle依赖即可
gradle引入:
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0-RC2")
//对应平台版本依赖,版本是一致的,如果想要切换到主线程来更新UI操作,就需要下面的依赖
//android
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC2")
//javafx
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.8.0-RC2")
//注意还得加入插件
plugins {
// For build.gradle.kts (Kotlin DSL)
kotlin("jvm") version "1.9.21"
// For build.gradle (Groovy DSL)
id "org.jetbrains.kotlin.jvm" version "1.9.21"
}
不过需要注意的是,上面的版本仅供参考
因为协程依赖与kotlin版本有关联关系,如果你使用协程库的高版本,可能kotlin也要使用较高版本,不然可能编译会报错
对于maven项目,修改项目使用的kotlin版本即可
对于gradle项目,除了修改kotlin版本,还得修改上面的那个plugin插件版本
不过稳妥的做法,还是根据kotlin版本选择对应的协程版本,毕竟没准kotlin版本一升级,整个项目就跑不起来,尤其是Android项目(kotlin版本依赖比较严重)
协程与kotlin版本对应关系见下表(点击展开)
发布时间 | kotlin版本 | 官方推荐的协程库版本 | 标准库更新版本简述 |
---|---|---|---|
2020-04-15 | 1.3.72 | 1.3.8 | Kotlin 1.3.70 的错误修复版本。 |
2020-08-17 | 1.4.0 | 1.3.9 | 具有许多功能和改进的功能版本,主要关注质量和性能。 |
2020-09-07 | 1.4.10 | 1.3.9 | Kotlin 1.4.0 的错误修复版本。 |
2020-11-23 | 1.4.20 | 1.4.1 | 支持新的 JVM 功能,例如通过调用动态进行字符串连接,改进了 KMM 项目的性能和异常处理,DK 路径的扩展:Path(“dir”) / “file.txt” |
2020-12-07 | 1.4.21 | 1.4.1 | Kotlin 1.4.20 的错误修复版本 |
2021-02-03 | 1.4.30 | 1.4.2 | 新的 JVM 后端,现在处于 Beta 版;新语言功能预览;改进的 Kotlin/Native 性能;标准库 API 改进 |
2021-02-25 | 1.4.31 | 1.4.2 | Kotlin 1.4.30 的错误修复版本 |
2021-03-22 | 1.4.32 | 1.4.3 | Kotlin 1.4.30 的错误修复版本 |
2021-05-05 | 1.5.0 | 1.5.0-RC | 具有新语言功能、性能改进和进化性更改(例如稳定实验性 API)的功能版本。 |
2021-05-24 | 1.5.10 | 1.5.0 | Kotlin 1.5.0 的错误修复版本。 |
2021-06-24 | 1.5.20 | 1.5.0 | 默认情况下,通过 JVM 上的调用动态进行字符串连接;改进了对 Lombok 的支持和对 JSpecify 的支持;Kotlin/Native:KDoc 导出到 Objective-C 头文件和更快的 Array.copyInto() 在一个数组中;Gradle:缓存注解处理器的类加载器并支持 --parallel Gradle 属性;跨平台的 stdlib 函数的对齐行为 |
2021-07-13 | 1.5.21 | 1.5.0 | Kotlin 1.5.20 的错误修复版本。 |
2021-08-23 | 1.5.30 | 1.5.1 | JVM上注解类的实例化;改进的选择加入要求机制和类型推断;测试版中的 Kotlin/JS IR 后端;支持 Apple Silicon 目标;改进的 CocoaPods 支持;Gradle:Java 工具链支持和改进的守护程序配置; |
2021-09-20 | 1.5.31 | 1.5.2 | Kotlin 1.5.30 的错误修复版本。 |
2021-11-29 | 1.5.32 | 1.5.2 | Kotlin 1.5.31 的错误修复版本。 |
2021-11-16 | 1.6.0 | 1.6.0 | 具有新语言功能、性能改进和进化性更改(例如稳定实验性 API)的功能版本。 |
2021-12-14 | 1.6.10 | 1.6.0 | Kotlin 1.6.0 的错误修复版本。 |
2022-04-04 | 1.6.20 | 1.6.0 | 具有各种改进的增量版本 |
2022-04-20 | 1.6.21 | 1.6.0 | Kotlin 1.6.20 的错误修复版本。 |
2022-06-09 | 1.7.0 | 1.7.0 | 在 Alpha for JVM 中发布了 Kotlin K2 编译器的功能、稳定的语言功能、性能改进和演进性变化,例如稳定实验性 API。 |
2022-07-07 | 1.7.10 | 1.7.0 | Kotlin 1.7.0 的错误修复版本。 |
2022-09-29 | 1.7.20 | 1.7.0 | 具有新语言功能的增量版本,支持 Kotlin K2 编译器中的多个编译器插件,默认启用新的 Kotlin/Native 内存管理器,以及对 Gradle 7.1 的支持。 |
2022-11-09 | 1.7.21 | 1.7.0 | Kotlin 1.7.20 的错误修复版本。 |
2022-12-28 | 1.8.0 | 1.7.0 | 一个功能版本,改进了 kotlin-reflect 性能、新的 JVM 递归复制或删除目录内容实验功能、改进了 Objective-C/Swift 互操作性。 |
2023-02-02 | 1.8.10 | 1.7.0 | Kotlin 1.8.0 的错误修复版本。 |
2023-04-03 | 1.8.20 | 1.7.0 | 功能发布,包括 Kotlin K2 编译器更新、AutoCloseable 接口和 stdlib 中的 Base64 编码、默认启用的新 JVM 增量编译、新的 Kotlin/Wasm 编译器后端。 |
2023-04-25 | 1.8.21 | 1.7.0 | Kotlin 1.8.20 的错误修复版本。 |
2023-06-08 | 1.8.22 | 1.7.0 | Kotlin 1.8.20 的错误修复版本。 |
2023-07-06 | 1.9.0 | 1.7.0 | 包含 Kotlin K2 编译器更新的功能版本、新的枚举类值函数、开放式范围的新运算符、Kotlin Multiplatform 中的 Gradle 配置缓存预览、Kotlin Multiplatform 中的 Android 目标支持更改、Kotlin/Native 中的自定义内存分配器预览 。 |
摘自:kotlin标准库与kotlin协程相关支持库对应关系(持续更新。。。)_kotlinx-coroutines-core和kotlin-gradle-plugin版本对应-CSDN博客
协程启动
先来一段协程启动的代码
fun main() {
runBlocking {
val scope = this
//启动协程
val job = scope.launch(Dispatchers.IO) {
delay(1000)
println("延迟1s后打印")
}
println("已启动协程了")
}
}
运行结果:
已启动协程了
延迟1s后打印
协程取消
fun main() {
runBlocking {
val scope = this
val job = scope.launch(Dispatchers.IO) {
delay(1000)
println("延迟1s后打印")
}
println("已启动协程了")
job.cancel()
println("已取消协程")
}
}
基础概念
协程主要包含以下部分:
- 协程Job:协程Job是协程的执行单元,它表示了一个协程的任务。我们可以通过Job来控制协程的启动、取消、等待和异常处理等操作。
- 协程构建器(Coroutine Builders):协程构建器是创建协程的入口点。在Kotlin中,常见的协程构建器有
launch
、async
、runBlocking
等。 - 协程作用域(Coroutine Scope):协程作用域是协程的生命周期范围。它定义了协程的生命周期和取消操作。通常,我们会使用
GlobalScope
、CoroutineScope
等来创建协程作用域。 - 协程执行器(Dispatcher):协程执行器(也称为调度器)是协程的执行线程(或线程池)。它决定了协程在哪个线程上执行,可以通过指定不同的调度器来实现协程的并发和异步操作。
- 协程挂起函数(Suspending Function):协程挂起函数是在协程中使用的特殊函数,它可以暂时挂起协程的执行而不阻塞线程。挂起函数使用
suspend
修饰符,并可调用其他挂起函数、阻塞函数、异步函数等。 - 协程上下文(Coroutine Context):协程上下文是协程的运行环境,包含了协程的调度器(Dispatcher)和其他上下文元素,如异常处理器等。协程上下文可以由调度器、Job、异常处理器等元素组成。
其实入门的简单使用,用的比较频繁的还是前5个概念,最后一个协程上下文概念我可能不会花太多笔墨写
协程上下文CoroutineContext实际是一个接口,而Job,Dispatcher都是实现了协程上下文此接口
首先,要有个概念,只要在协程作用域中才能启动协程,而协程作用域,需要通过协程构建器来进行创建
我们来看上面的代码
fun main() {
//runBlocking方法实际上就是协程构建器
runBlocking {
//这里的作用域实际就是协程作用域
val scope = this
//通过launch方法来启动一个协程,得到一个Job对象
//实际上,把Job对象说成协程应该就好理解了
//注意这里,出现了一个Dispatchers.IO,这个就是我们的协程执行器,可以看做为一个协程提供的线程池(之后会细讲)
val job = scope.launch(Dispatchers.IO) {
//delay是延迟执行,是协程作用域提供的一个方法
delay(1000)
println("延迟1s后打印")
}
println("已启动协程了")
}
}
通过上面的代码,应该对前4个概念有些基本了解了,再来说说挂起函数
以上面代码为例,协程里的方法太多了,想要封装成一个方法,可以这样改造:
fun main() {
runBlocking {
val scope = this
val job = scope.launch(Dispatchers.IO) {
test()
}
println("已启动协程了")
}
}
suspend fun test() {
delay(1000)
println("延迟1s后打印")
}
由于我们因为用到了delay这个方法,所以我们得将当前方法加上一个suspend
关键字,声明当前函数是挂起函数
只有声明了我们才能在函数里使用delay这个方法,不加关键字,IDE会提示行代码标红,无法通过编译
同时,还有一个概念,只有在协程作用域上,才能调用挂起函数
当然,如果你的方法里没有delay此类方法,可以不加suspend关键字声明
协程作用域提供了不止delay这个方法,还有些其他方法,下文会进行补充
至于最后一个协程上下文,我们可以runBlocking和launch方法参数见到它的身影如下图:
由于本文偏向使用为主,所以不打算对协程上下文进行展开细说了
协程构建器
前面也说到了,runBlocking()可以看做为一个协程构建器,但这个只是方便我们在main方法或者测试使用,为什么呢?
因为它实际上会阻塞当前线程,如下代码:
fun main() {
runBlocking() {
val scope = this
val job = scope.launch(Dispatchers.IO) {
delay(1000)
println("延迟1s后打印")
}
println("已启动协程了")
}
println("任务结束")
}
输出结果:
已启动协程了
延迟1s后打印
任务结束
由输出结果可以看出,当前main方法需要等待runBlocking()
方法及里面协程执行完毕才会执行完毕
但是像Android开发和Javafx开发,如果想上述这样写法,在runBlocking()
进行耗时长的任务,那么估计UI线程直接卡死,Android直接出现ANR异常了
那么问题来了,协程提供了哪些协程构造器?
答案如下:
runBlocking
launch
async
runBlocking
: 会创建一个新的协程同时阻塞当前线程,直到协程结束。适用于main函数和单元测试
需要注意的是,runBlocking会根据最后一行从而返回数值,类似kotlin对象的run函数,如
fun main() {
val str = runBlocking() {
//省略协程启动等操作
"hello"
}
//返回字符串
println(str)
}
launch
: 创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。它返回的是一个该协程任务的引用,即Job对象。这是最常用的启动协程的方式。
async
: 创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用,并返回Deffer对象。可通过调用Deffer.await()方法等待该子协程执行完成并获取结果。常用于并发执行-同步等待和获取返回值的情况。
由于launch
和async
2个构造器得需要和协程作用域配合使用,所以决定在下面和协程作用域一起讲解了
协程和协程作用域
协程作用域
如果在一段普通代码想要开启协程,除了上面说到的runBlocking方法,我们还可以通过协程作用域来调用launch
和async
来进行协程的启动
可用的协程作用域有:
GlobalScope
CoroutineScope
supervisorScope{}
好像低版本只有方法,而高版本的协程库则可以使用类SupervisorScope
MainScope
主线程协程作用域(需要引用对应平台的依赖,如android或javafx才会有此作用域)
其中GlobalScope是一个全局的协程作用域对象,使用的话,直接使用静态方法来进行,如下代码:
GlobalScope.launch {
//你的逻辑
}
不过这种启动的协程存在组件被销毁但协程还存在的情况,一般不推荐
而一般推荐使用新建一个CoroutineScope对象来启动协程,之后在组件销毁的生命周期手动调用cancel()
方法,会将当前所有的协程任务都取消,如下代码:
//在当前类声明此对应(如Activity)
val scope = CoroutineScope(Dispatchers.Main)
//这里在按钮点击事件里执行
//这里使用的协程调度器指定当前协程作用域是在主线程(UI线程)
scope.launch{
}
//在组件销毁的生命周期(如Activity的onDestroy方法里)
scope.cancel()
SupervisorScope这个协程作用域主要是针对异常的,如果子协程发生异常,则不影响父协程的运行(具体可见下文的"协程里的异常"一章),这里先不介绍
MainScope主要是UI主线程的协程作用域,在此作用域,相当于在主线程操作,一般我们将耗时操作切换到Dispatchers.IO去做,如下代码:
MainScope().launch{
withContext(Dispatchers.IO){
//网络请求等耗时操作
}
//更新UI操作
}
上面的withContext()
方法也是在协程作用域才能使用的方法,目的就是切换到其他协程执行耗时操作,执行完毕后再切换回当前的协程(主线程),是个阻塞操作
如果需要根据网络请求的结果从而来进行更新UI,可以利用withContext()的返回值,如将上述代码改造如下:
MainScope().launch{
val str = withContext(Dispatchers.IO){
//网络请求等耗时操作
//假设得到一个字符串返回值
"hello"
}
//更新UI操作
tv.text = str
}
PS:如果对于Android平台,还可以使用下面的2个作用域:
lifecycleScope
:生命周期范围,用于activity等有生命周期的组件,在DESTROYED的时候会自动结束,需要导入依赖implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
viewModelScope
:viewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束,需要导入依赖implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
介绍完上面几个协程作用域后,接下来对launch方法和async方法进行讲解
调度器
在讲解launch方法之前,先讲解下调度器的种类和概念
首先,我们知道此launch方法返回job对象,之后我们可以对job对象操作,调用job.cancel()
取消任务
而launch方法里的传参,可以传递一个调度器,那么协程中有哪几个调度器?
主要有以下几个:
Dispatchers.DEFAULT
Dispatchers.IO
Dispatchers.MAIN
简单理解调度器视为线程就比较好理解了,比如说我们需要执行长时间的任务,就使用Dispatchers.IO
调度器,而需要更改UI,则切换回主线程,如下面代码示例:
btn.setOnClicker{
//按钮点击触发协程
val job = CoroutineScope(Dispatchers.Main).launch(Dispatchers.Main){
val result = withContext(Dispatchers.IO){
//模拟请求数据,最终得到数据
"resp"
}
//根据result来进行更改UI操作(这里已经在主线程了)
textview.text = "result"
}
}
就像之前所说,我们在普通代码中使用launch,就得先创建一个协程作用域CoroutineScope,之后再启动一个协程
CoroutineScope的构造方法需要传一个协程调度器,这里我们就是传了Dispatchers.MAIN
,标示此协程作用域默认是在主线程
之后我们也可以通过launch方法来切换不同的线程执行,上面代码中,CoroutineScope和launch都有设置一个调度器
实际上,Dispatchers.MAIN是一个对象,上面代码我们可以省略launch方法里参数,如下代码
btn.setOnClicker{
//按钮点击触发协程
val job = CoroutineScope(Dispatchers.Main).launch{
val result = withContext(Dispatchers.IO){
//模拟请求数据,最终得到数据
"resp"
}
//根据result来进行更改UI操作(这里已经在主线程了)
textview.text = "result"
}
}
如果引用了Javafx的依赖,那么这个Dispatchers.MAIN
和Dispatchers.JAVAFX
是一个对象,两者可互用
再来说说Dispatchers.IO
,实际上这个是类似线程池的东西,创建的协程任务可能会被分配到不同的协程上去执行
协程实际也有有个线程池的,只不过我们使用可以不太关心,当然,如果你需要自己构建一个线程池给协程使用,也有对应方法可以设置,如下方法
// 创建一个包含多个线程的线程池
val customThreadPool = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
runBlocking {
//启动的设置
launch(customThreadPool) {
}
//或者
withContext(customThreadPool) {
repeat(10) {
println("Coroutine is running on thread: ${Thread.currentThread().name}")
}
}
}
// 关闭线程池
(customThreadPool.executor as? Executors)?.shutdown()
asCoroutineDispatcher()
方法,是协程为传统线程池提供的一个扩展方法,可以将线程池转为我们的Dispatcher进行使用(用法方面,和Dispatchers.Main
这种对象使用一样)
launch方法
其实关于launch()的使用方法,上面的例子已经介绍的七七八八了,主要是对关于launch()返回的Job对象进行讲解
Job对象有以下常用属性和方法:
-
isActive
当前协程是否已经激活 -
isCompleted
当前协程是否已经完成 -
isCancelled
当前协程是否已经取消 -
cancel()
用于Job的取消,取消协程 -
start()
用于启动一个协程,让其到达Active状态 -
invokeOnCompletion()
当其完成或者异常时会调用 -
join()
阻塞并等候当前协程完成
前3个属性很好理解,这里直接跳过;
注意到有一个start()方法,什么意思呢?因为协程可以设置为懒启动,具体代码如下:
val job = launch(start = CoroutineStart.LAZY) { }
job.start()
而关于CoroutineStart
类,有以下几种选中
DEFAULT
:默认启动模式,表示协程会立即开始执行。(之前省略不写,就是使用的这个选项)LAZY
:懒启动模式,表示协程只有在首次被使用时才会启动执行。ATOMIC
:原子启动模式,表示协程会尽快被执行,但可以被取消。UNDISPATCHED
:未调度启动模式,表示协程会在当前调用者线程中立即执行,而不进行调度。
至于后2种,目前我没有在具体情景使用,只是做个了解,不扩展进行说明了
invokeOnCompletion方法则是方便我们监听协程完成后的操作,具体示例代码如下:
val job = launch() { }
job.invokeOnCompletion{
//相关逻辑
}
这里通过IDE的代码提示,可以看见invokeOnCompletion方法还可以接受2个参数
onCancelling
job被取消是否触发当前回调,默认为falseinvokeImmediately
指示指定的代码块是否应立即调用,而不管作业状态如何,默认为true
上面列的几个方法只是常用的,还有些不常用的方法,由于自己不怎么常用,这里就不一一来列出来了
协程并发
async方法
如果说,我们想要实现几个协程并发进行,就可以使用此方法来开启多个协程,如下例子
runBlocking {
async() {
//逻辑1
}
async() {
//逻辑2
}
}
async方法参数和launch方法是一样的,用法方面我这里就不多说什么了,唯一需要注意的是,async方法返回的是一个Deffer对象(虽然它也是继承于Job对象)
如果我们需要等待某个方法的结果的话,可以使用Deffer.await()方法来实现,如下面例子:
runBlocking {
val deffer = async {
delay(200)
5 //这里语法上是kotlin的作用域方法,返回一个int类型,如果不明白的可以自行去了解下
}
val result = deffer.await() // result为Int类型,数据为5
}
await()
调用后,会使当前协程作用域进行等待,直到协程执行完毕
由于Deffer对象是继承于Job对象,所有Job的相关方法,它也可以用,这里参考上面说到的Job的相关方法即可
最后补充下:
如果我们需要协程并发比较多的话,可以使用一个list来装Deffer对象,最后统一调用await()
方法,代码如下:
runBlocking {
val list = (0..10).map {
async {
delay(200)
5
}
}
list.forEach {
//每个协程执行结果,做对应逻辑操作
val result = it.await()
}
}
不过看到某大佬的文章,提到:协程并发并不是指线程并发,
上面代码实际也可以使用launch方法来实现并发,详见此文Kotlin协程-协程的日常进阶使用 - 掘金
父协程和子协程
还记得上面提到的协程取消方法吗?协程取消,会同时将其有关联的子协程全部依次取消,具体代码:
runBlocking {
val job1 = launch {
val deffer = async {
}
val job2 = launch { }
}
job1.cancel()
}
如上面示例,job1为父协程,deffer和job2为子协程,当父协程取消,同时deffer和job2也会取消
这里还有一点要说明:
协程的异常是会传递的,比如当一个子协程发生异常时,它会影响它的兄弟协程与它的父协程。而使用了 SupervisorJob() 则意味着,其子协程的异常都将由其自己处理,而不会向外扩散,影响其他协程。
详情文章解释可参考此文Kotlin | 关于协程异常处理,你想知道的都在这里 - 掘金,本文不扩展说明了
一般这样定义一个作用域即可解决问题,代码如下:
private val exceHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e("tttt", "协程发生异常", throwable)
}
//调度器Dispatchers.IO根据你自己需要来即可
val gCo = CoroutineScope(SupervisorJob() + Dispatchers.IO + exceHandler)
扩展补充
传统java接口回调如何转协程同步写法
之前一直有个痛点,就是用的是Java库,里面提供的异步操作结果都是通过接口回调的方式来返回数据的,如果我们kotlin中也是去这样写的话,根本就没法体验到协程的优势
kotlin协程,则是提供了一个高级函数suspendCancellableCoroutine{}
供我们解决上述问题
这里以一个简单的网络请求为例,有2个接口回调,分别代表请求成功和请求失败
interface RespInterface {
fun onSuccess(data:String)
fun onError()
}
Net.post(object :RespInterface{
override fun onSuccess(data: String) {
}
override fun onError() {
}
})
使用suspendCancellableCoroutine{}
改造,代码:
suspend fun myJob() = suspendCancellableCoroutine<String> {
//下面的it代表CancellableContinuation<String>对象
Net.post(object :RespInterface{
override fun onSuccess(data: String) {
it.resume(data){}
}
override fun onError() {
it.resume(""){}
}
})
}
//在协程中调用
runBlocking {
//result为对应的返回结果
val result = postNet()
}
suspendCancellableCoroutine{}
返回的是CancellableContinuationresume
方法返回的对象类型来定义
上面我只是一个简单的例子,如果请求失败,则返回一个空白字符串,到时候逻辑在协程里判断即可
对话框按顺序弹出(Android)
这个同理,也是根据上面的suspendCancellableCoroutine{}
方法来实现的,就是有点麻烦,得每个对话框的方法都单独写
下面代码是在Android平台上使用的,使用DialogX库的里的提示框作为示例:
suspend fun showDialog1() = suspendCancellableCoroutine<String> {
MessageDialog.show("提示1","提示1","确定")
.setOkButton { dialog, v ->
false
}
.setDialogLifecycleCallback(object :DialogLifecycleCallback<MessageDialog>(){
override fun onDismiss(dialog: MessageDialog?) {
it.resume(""){}
super.onDismiss(dialog)
}
})
}
suspend fun showDialog2() = suspendCancellableCoroutine<String> {
MessageDialog.show("提示2","提示2","确定")
.setOkButton { dialog, v ->
false
}
.setDialogLifecycleCallback(object :DialogLifecycleCallback<MessageDialog>(){
override fun onDismiss(dialog: MessageDialog?) {
it.resume(""){}
super.onDismiss(dialog)
}
})
}
//使用
lifecycleScope.launch {
showDialog1()
showDialog2()
}
如何自定义一个协程作用域
可以直接让我们的类实现 CoroutineScope 接口,但是我们需要指定协程的上下文,如下面代码:
/**
* 自定义带协程作用域的弹窗
*/
abstract class CoroutineScopeCenterPopup(activity: FragmentActivity) : CenterPopupView(activity), CoroutineScope {
private lateinit var job: Job
private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
YYLogUtils.e(throwable.message ?: "Unkown Error")
}
//此协程作用域的自定义 CoroutineContext
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + CoroutineName("CenterPopupScope") + exceptionHandler
override fun onCreate() {
job = Job()
super.onCreate()
}
override fun onDismiss() {
job.cancel() // 关闭弹窗后,结束所有协程任务
YYLogUtils.w("关闭弹窗后,结束所有协程任务")
super.onDismiss()
}
}
上文代码摘抄自Kotlin协程-协程的日常进阶使用 - 掘金,仅供记录方便后来查阅参考
协程常用高阶函数
协程里提供了一些函数使用,上面应该已经介绍的差不多了
//创建一个普通的CoroutineScope
coroutineScope {}
//使用SupervisorJob()创建一个CoroutineScope
supervisorScope{}
//执行一个挂起函数,如果超时,抛出TimeoutCancellationException异常!
withTimeout(time Millis: 1000){}
//执行一个挂起函数,如果超时,返回null
withTimeoutorNull(time Millis: 1000) {}
//挂起当前协程,直到协程执行完成,如果传递的context与当前context一致,则该函数不会挂起,相当于阻塞执行
withContext(Dispatchers.I0) {}
//一个方便的可取消的协程作用域
suspendCancellableCoroutine{}
参考
- kotlinx.coroutines/README.md at master · Kotlin/kotlinx.coroutines
- 协程基础 · Kotlin 官方文档 中文版
- Kotlin | 关于协程异常处理,你想知道的都在这里 - 掘金
- 写给Android工程师的协程指南 - 掘金
- Kotlin协程解析系列(上):协程调度与挂起 - 知乎
- 如何通过Kotlin协程, 简化"连续依次弹窗(Dialog队列)"的需求 - 掘金
- Kotlin协程-CoroutineScope协程作用域 - 掘金
- Kotlin协程-协程的日常进阶使用 - 掘金
评论区