Kotlin 协程基础使用学习

原文: 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-androidkotlinx-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-androidkotlinx-coroutines-javafx依赖

引入依赖(示例):

<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core-jvm</artifactId>
    <version>1.8.0-RC2</version>
</dependency>

//这里省略了对应平台的版本依赖,参考下面gradle依赖即可

gradle引入:

groovy 复制代码
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博客

协程启动

先来一段协程启动的代码

kotlin 复制代码
fun main() {
    runBlocking {
        val scope = this
		//启动协程
        val job = scope.launch(Dispatchers.IO) {
            delay(1000)
            println("延迟1s后打印")
        }
        println("已启动协程了")
    }
}

运行结果:

已启动协程了
延迟1s后打印

协程取消

kotlin 复制代码
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中,常见的协程构建器有launchasyncrunBlocking等。
  • 协程作用域(Coroutine Scope):协程作用域是协程的生命周期范围。它定义了协程的生命周期和取消操作。通常,我们会使用GlobalScopeCoroutineScope等来创建协程作用域。
  • 协程执行器(Dispatcher):协程执行器(也称为调度器)是协程的执行线程(或线程池)。它决定了协程在哪个线程上执行,可以通过指定不同的调度器来实现协程的并发和异步操作。
  • 协程挂起函数(Suspending Function):协程挂起函数是在协程中使用的特殊函数,它可以暂时挂起协程的执行而不阻塞线程。挂起函数使用suspend修饰符,并可调用其他挂起函数、阻塞函数、异步函数等。
  • 协程上下文(Coroutine Context):协程上下文是协程的运行环境,包含了协程的调度器(Dispatcher)和其他上下文元素,如异常处理器等。协程上下文可以由调度器、Job、异常处理器等元素组成。

其实入门的简单使用,用的比较频繁的还是前5个概念,最后一个协程上下文概念我可能不会花太多笔墨写

协程上下文CoroutineContext实际是一个接口,而Job,Dispatcher都是实现了协程上下文此接口

首先,要有个概念,只要在协程作用域中才能启动协程 ,而协程作用域,需要通过协程构建器来进行创建

我们来看上面的代码

kotlin 复制代码
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个概念有些基本了解了,再来说说挂起函数

以上面代码为例,协程里的方法太多了,想要封装成一个方法,可以这样改造:

kotlin 复制代码
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方法或者测试使用,为什么呢?

因为它实际上会阻塞当前线程,如下代码:

kotlin 复制代码
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函数,如

kotlin 复制代码
fun main() {
    val str = runBlocking() {
		//省略协程启动等操作
        "hello"
    }
	//返回字符串
    println(str)
}

launch : 创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。它返回的是一个该协程任务的引用,即Job对象。这是最常用的启动协程的方式。

async: 创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用,并返回Deffer对象。可通过调用Deffer.await()方法等待该子协程执行完成并获取结果。常用于并发执行-同步等待和获取返回值的情况。

由于launchasync2个构造器得需要和协程作用域配合使用,所以决定在下面和协程作用域一起讲解了

协程和协程作用域

协程作用域

如果在一段普通代码想要开启协程,除了上面说到的runBlocking方法,我们还可以通过协程作用域来调用launchasync来进行协程的启动

可用的协程作用域有:

  • GlobalScope
  • CoroutineScope
  • supervisorScope{} 好像低版本只有方法,而高版本的协程库则可以使用类SupervisorScope
  • MainScope 主线程协程作用域(需要引用对应平台的依赖,如android或javafx才会有此作用域)

其中GlobalScope是一个全局的协程作用域对象,使用的话,直接使用静态方法来进行,如下代码:

kotlin 复制代码
GlobalScope.launch { 
	//你的逻辑
}

不过这种启动的协程存在组件被销毁但协程还存在的情况,一般不推荐

而一般推荐使用新建一个CoroutineScope对象来启动协程,之后在组件销毁的生命周期手动调用cancel()方法,会将当前所有的协程任务都取消,如下代码:

kotlin 复制代码
//在当前类声明此对应(如Activity)
val scope = CoroutineScope(Dispatchers.Main)

//这里在按钮点击事件里执行
//这里使用的协程调度器指定当前协程作用域是在主线程(UI线程)
scope.launch{
	
}

//在组件销毁的生命周期(如Activity的onDestroy方法里)
scope.cancel()

SupervisorScope这个协程作用域主要是针对异常的,如果子协程发生异常,则不影响父协程的运行(具体可见下文的"协程里的异常"一章),这里先不介绍

MainScope主要是UI主线程的协程作用域,在此作用域,相当于在主线程操作,一般我们将耗时操作切换到Dispatchers.IO去做,如下代码:

kotlin 复制代码
MainScope().launch{
	withContext(Dispatchers.IO){
		//网络请求等耗时操作
	}
	//更新UI操作
}

上面的withContext()方法也是在协程作用域才能使用的方法,目的就是切换到其他协程执行耗时操作,执行完毕后再切换回当前的协程(主线程),是个阻塞操作

如果需要根据网络请求的结果从而来进行更新UI,可以利用withContext()的返回值,如将上述代码改造如下:

kotlin 复制代码
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,则切换回主线程,如下面代码示例:

kotlin 复制代码
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方法里参数,如下代码

kotlin 复制代码
btn.setOnClicker{
	//按钮点击触发协程
	val job = CoroutineScope(Dispatchers.Main).launch{
		val result = withContext(Dispatchers.IO){
			//模拟请求数据,最终得到数据
			"resp"
		}
		//根据result来进行更改UI操作(这里已经在主线程了)
		textview.text = "result"
	}
}

如果引用了Javafx的依赖,那么这个Dispatchers.MAINDispatchers.JAVAFX是一个对象,两者可互用

再来说说Dispatchers.IO,实际上这个是类似线程池的东西,创建的协程任务可能会被分配到不同的协程上去执行

协程实际也有有个线程池的,只不过我们使用可以不太关心,当然,如果你需要自己构建一个线程池给协程使用,也有对应方法可以设置,如下方法

kotlin 复制代码
// 创建一个包含多个线程的线程池
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()方法,什么意思呢?因为协程可以设置为懒启动,具体代码如下:

kotlin 复制代码
val job = launch(start = CoroutineStart.LAZY) {  }
job.start()

而关于CoroutineStart类,有以下几种选中

  • DEFAULT:默认启动模式,表示协程会立即开始执行。(之前省略不写,就是使用的这个选项)
  • LAZY:懒启动模式,表示协程只有在首次被使用时才会启动执行。
  • ATOMIC:原子启动模式,表示协程会尽快被执行,但可以被取消。
  • UNDISPATCHED:未调度启动模式,表示协程会在当前调用者线程中立即执行,而不进行调度。

至于后2种,目前我没有在具体情景使用,只是做个了解,不扩展进行说明了

invokeOnCompletion方法则是方便我们监听协程完成后的操作,具体示例代码如下:

kotlin 复制代码
val job = launch() {  }
job.invokeOnCompletion{
	//相关逻辑
}

这里通过IDE的代码提示,可以看见invokeOnCompletion方法还可以接受2个参数

  • onCancelling job被取消是否触发当前回调,默认为false
  • invokeImmediately 指示指定的代码块是否应立即调用,而不管作业状态如何,默认为true

上面列的几个方法只是常用的,还有些不常用的方法,由于自己不怎么常用,这里就不一一来列出来了

协程并发

async方法

如果说,我们想要实现几个协程并发进行,就可以使用此方法来开启多个协程,如下例子

kotlin 复制代码
runBlocking {
	async() {
		//逻辑1
	}
	async() {
		//逻辑2
	}
}

async方法参数和launch方法是一样的,用法方面我这里就不多说什么了,唯一需要注意的是,async方法返回的是一个Deffer对象(虽然它也是继承于Job对象)

如果我们需要等待某个方法的结果的话,可以使用Deffer.await()方法来实现,如下面例子:

kotlin 复制代码
runBlocking {
	val deffer = async {
		delay(200)
		5 //这里语法上是kotlin的作用域方法,返回一个int类型,如果不明白的可以自行去了解下
	}
   val result = deffer.await() // result为Int类型,数据为5
}

await()调用后,会使当前协程作用域进行等待,直到协程执行完毕

由于Deffer对象是继承于Job对象,所有Job的相关方法,它也可以用,这里参考上面说到的Job的相关方法即可

最后补充下:

如果我们需要协程并发比较多的话,可以使用一个list来装Deffer对象,最后统一调用await()方法,代码如下:

kotlin 复制代码
runBlocking {

	val list = (0..10).map {
		async {
			delay(200)
			5
		}
	}
	
	list.forEach { 
		//每个协程执行结果,做对应逻辑操作
		val result = it.await()
	}
}

不过看到某大佬的文章,提到:协程并发并不是指线程并发,

上面代码实际也可以使用launch方法来实现并发,详见此文Kotlin协程-协程的日常进阶使用 - 掘金

父协程和子协程

还记得上面提到的协程取消方法吗?协程取消,会同时将其有关联的子协程全部依次取消,具体代码:

kotlin 复制代码
runBlocking {
	val job1 = launch {
		val deffer = async {
		}

		val job2 = launch {  }
	}
	job1.cancel()
}

如上面示例,job1为父协程,deffer和job2为子协程,当父协程取消,同时deffer和job2也会取消

这里还有一点要说明:

协程的异常是会传递的,比如当一个子协程发生异常时,它会影响它的兄弟协程与它的父协程。而使用了 SupervisorJob() 则意味着,其子协程的异常都将由其自己处理,而不会向外扩散,影响其他协程。

详情文章解释可参考此文Kotlin | 关于协程异常处理,你想知道的都在这里 - 掘金,本文不扩展说明了

一般这样定义一个作用域即可解决问题,代码如下:

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个接口回调,分别代表请求成功和请求失败

kotlin 复制代码
interface RespInterface {
	fun onSuccess(data:String)

	fun onError()
}

Net.post(object :RespInterface{
	override fun onSuccess(data: String) {
	}

	override fun onError() {
	}
})

使用suspendCancellableCoroutine{}改造,代码:

kotlin 复制代码
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{}返回的是CancellableContinuation 对象,这里的T类型,就是看你最终调用 resume方法返回的对象类型来定义

上面我只是一个简单的例子,如果请求失败,则返回一个空白字符串,到时候逻辑在协程里判断即可

对话框按顺序弹出(Android)

这个同理,也是根据上面的suspendCancellableCoroutine{}方法来实现的,就是有点麻烦,得每个对话框的方法都单独写

下面代码是在Android平台上使用的,使用DialogX库的里的提示框作为示例:

kotlin 复制代码
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 接口,但是我们需要指定协程的上下文,如下面代码:

kotlin 复制代码
/**
 * 自定义带协程作用域的弹窗
 */
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协程-协程的日常进阶使用 - 掘金,仅供记录方便后来查阅参考

协程常用高阶函数

协程里提供了一些函数使用,上面应该已经介绍的差不多了

kotlin 复制代码
//创建一个普通的CoroutineScope
coroutineScope {}

//使用SupervisorJob()创建一个CoroutineScope
supervisorScope{}

//执行一个挂起函数,如果超时,抛出TimeoutCancellationException异常!
withTimeout(time Millis: 1000){}

//执行一个挂起函数,如果超时,返回null
withTimeoutorNull(time Millis: 1000) {}

//挂起当前协程,直到协程执行完成,如果传递的context与当前context一致,则该函数不会挂起,相当于阻塞执行
withContext(Dispatchers.I0) {}

//一个方便的可取消的协程作用域
suspendCancellableCoroutine{}

参考