Kotlin 协程
Coroutines(协程)是一种编程思想,并不局限于特定的语言。
协程可以使用阻塞的方式写出非阻塞式的代码,解决并发中常见的回调地狱。消除了并发任务之间的协作的难度。
在 android 中使用
引入依赖
root build.gradle
plugins {
// kotlin编译插件
id 'org.jetbrains.kotlin.android' version '1.5.30' apply false
}
app build.gradle
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1")// 协程核心库
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1")// 协程android支持库
创建协程
- 方式一:使用 runBlocking 顶层函数,线程阻塞,一般用于单元测试
- 方式二:使用 GlobalScope 单例对象,和应用生命周期一致,一般不推荐使用
- 方式三:通过 CoroutineContext(协程上下文) 创建一个 CoroutineScope(协程作用域) 对象
// 使用顶层函数 runBlocking 启动协程
runBlocking {
// 协程的内容
}
// GlobalScope 启动协程
GlobalScope.launch {
// 协程的内容
}
// 通过 协程上下文创建协程作用域,然后启动协程
val coroutineScope = CoroutineScope(Job() + Dispatchers.Main)
coroutineScope.launch {
// 协程的内容
}
CoroutineContext(协程上下文)
表示协程的运行环境,是一组定义协程行为的元素,它由以下几项组成:
- Job:协程任务,可以取消,用于控制协程的生命周期
- Dispatcher:协程调度器,用于切换协程中当前任务执行的线程
- CoroutineName:协程名称,可以自定义协程的名称,用于调试日志输出
- CoroutineExceptionHandler:协程异常处理器,处理未被捕获的异常
创建 CoroutineScope
有时我们需要在协程上下文中定义多个元素,组合协程上下文中的元素,使用 + 操作符来创建 CoroutineScope .
CoroutineScope(Job() + Dispatchers.Main + CoroutineName("my_coroutine_1"))
CoroutineScope(SupervisorJob() + Dispatchers.IO + CoroutineName("my_coroutine_2"))
Job(协程任务)
Job 是 launch 构建协程返回的一个协程任务,Job 具有生命周期并且可以取消。
- isActive:当前协程是否处于活跃状态。
- isCompleted:当前协程任务是否已经结束,已取消、已失败和已完成都被视为已经结束。
- isCancelled:当前协程任务是否处于已取消状态。
CoroutineScope(协程作用域)
定义协程必须指定其 CoroutineScope,它会跟踪所有协程,同样它也可以取消由它启动的协程。
- GlobalScope:生命周期是应用级别的。
- MainScope:一个顶层函数,上下文是 SupervisorJob() + Dispatchers.Main,说明它是一个在主线程执行的协程作用域,通过 cancel 对协程进行取消。
- viewModelScope: 只能在 ViewModel 中使用,绑定 ViewModel 的生命周期。
- lifecycleScope: 只能在 Activity、Fragment 中使用,会绑定 Activity、Fragment 生命周期。
CoroutineStart(协程启动模式)
- DEFAULT:默认的启动模式,协程创建后,立即开始调度,在调度前如果协程被取消,其将直接进入取消响应的状态。
- ATOMIC:协程创建后,立即开始调度,协程执行到第一个挂起点之前不响应取消。
- LAZY:只有协程被需要时,包括主动调用协程的 start、join 或 await 等函数时才会开始调度,如果调度前就被取消,那么该协程将直接进入异常结束状态。
- UNDISPATCHED:协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点。
Dispatchers(协程调度器)
- Default:默认调度器,用于处理 CPU 密集型任务,后台线程
- Main:主线程调度器,用于刷新 UI
- Unconfigned:不局限于任何特定线程的协程调度器
- IO:IO调度器,用于 IO 相关的操作处理,后台线程
launch 和 async
CoroutineScope 的顶层函数,都是用来启动一个协程。
- launch:返回一个 Job 并且不附带任何结果值。
- async:返回一个 Deferred,Deferred 也是一个 Job,可以使用 await() 函数获得该协程返回的结果。常用于并发执行,同步等待和获取返回值的情况。
runBlocking {
val job1 = launch {
// 协程的内容
}
val job2 = async {
//协程的内容
"SUCCESS"// 最后一行 为该协程返回的结果
}
val result = job2.await()// 调用 await 会挂起,等待协程任务的返回
}
协程的作用域构建器
coroutineScope / supervisorScope
都是一个挂起函数,需要在协程中或挂起函数中创建。用于构建一个独立的协程作用域,等待其协程体以及所有子协程结束。
- coroutineScope:作用域中的任何一个协程失败了,其他协程也会被取消。
- supervisorScope:作用域中的子协程的异常不会影响到其他协程。
协程的取消
- 取消 CoroutineScope (协程作用域),会取消它的子协程。
- 取消 CoroutineScope (协程作用域)中的子协程,并不会影响其余兄弟协程。
- 协程通过抛出一个特殊的异常 CancellationException 来处理取消操作。
- 所有的挂起函数(withContext、delay 等)都是可取消的。
协程取消的副作用
可能会造成申请的资源没有释放,解决方法:
- 使用 try…finally 释放资源。
- 使用 use 函数,该函数只能被实现了 Closeble 的对象使用,程序结束后会自动调用 close 方法。
CPU 密集型任务取消
由于 CPU 密集型任务在 Job 取消的时候,不会被感知到,可能通过以下方式判断当前任务是否被取消:
- isActive 是一个可以被使用在 CoroutineScope 中的扩展属性,检查 Job 是否处于活跃状态。
- ensureActive(),如果 Job 处于非活跃状态,这个方法会立即抛出 CancellationException 予以响应 。
- yield() ,如果 Job 处于非活跃状态,这个方法会立即抛出 CancellationException 予以响应 。此外它还会让出线程的执行权,给其他协程执行机会。
协程上下文的继承
对于新创建的协程,它的 CoroutineContext 会包含一个全新的 Job 实例。剩下的元素会从 CoroutineContext 的父类继承,该父类可能是另外一个协程或创建该协程的 CoroutineScope .
Job / SupervisorJob
-
Job:一个子协程的失败会影响其他协程。
-
SupervisorJob:一个子协程的失败不会影响其他子协程,不会传播异常给它的父级,它会让子协程自己处理异常。
异常的捕获
-
使用 CoroutineExceptionHandler 对协程异常进行捕获
-
以下的条件被满足时,异常就会被捕获
- 时机:异常是被自动抛出异常的协程所抛出的(使用 launch,而不是 async 时);
- 位置:在 CoroutineScope 的 CoroutineContext 中 或 一个根协程(CoroutineScope 或 supervisorScope 的直接协程)中
异常的传播特性
当一个协程由于一个异常而运行失败,它会传播这个异常并传递给它的父级,接下来父级会进行下面的步骤:
- 取消它自己的子级
- 取消它自己
- 将异常传播并传递给他的父级
取消与异常
- 取消与协程紧密相关,协程内部抛出 CancellationException 来进行取消,这个异常会被忽略。
- 当子协程被取消时,不会取消它的父协程。
- 如果一个协程遇到了 CancellationException 以外的异常,它将使用异常取消它的父协程。当父协程的所有子协程都结束后,异常才会被父协程处理。
withContext
用于切换到指定的线程,并在闭包内的逻辑执行结束后,会自动恢复到线程切换之前继续执行。
coroutineScope.launch(Dispatchers.Main) {// UI线程启动
val image = withContext(Dispatchers.IO) {// 切换到IO线程
getImage(imageId)
}
avatarIv.setImageBitamp(image)// 回到UI线程更新
}
suspend
suspend 表明当前函数是一个挂起函数,需要在协程或其他挂起函数中执行。代码执行到 suspend 函数时会挂起,这个挂起是非阻塞式的,它不会阻塞当前的线程。当挂起函数执行完毕后,会恢复到之前的协程中继续执行。
Flow
异步数据流。是一种类似于序列的冷流,flow 构建器中的代码直到流被收集的时候才运行。
Flow 与其他方式的区别
- 名为 flow 的 Flow 类型构建器函数
- flow{…} 构建块中的代码可以挂起
- 函数不再标有 suspend 修饰符
- 流使用 emit 函数发射值
- 流使用 collect 函数收集值
流构建器
- flowOf 构建器定义了一个发射固定值集的流
- 使用 .asFlow() 扩展函数,可以将各种集合与序列转换为流
流上下文
-
流的收集总是在调用协程的上下文中发生,流的该属性称为 上下文保存。
-
flow{…} 构建器中的代码必须遵循上下文保存属性,并且不允许从其他上下文发射(emit)。
-
flowOn 操作符,该函数用于更改流发射的上下文。
启动流
使用 launchIn 替换 collect 可以在单独的协程中启动流的收集。
取消流
流采用与协程同样的协作取消。像往常一样,流的收集可以是当流在一个可取消的挂起函数(例如 delay)中挂起的时候取消。
流的取消检测
- 流构建器对每个发射值执行附加的 ensureActive 检测以进行取消,这意味从 flow{…} 发出的繁忙循环是可以取消的。
- 出于性能原因,大多数其他流操作不会自行执行其他取消检测,在协程处于繁忙循环的情况下,必须明确检测是否取消。
- 通过 cancellable 操作来执行此操作。
被压
生产者的生产效率大于消费者的消费效率
- buffer(),并发运行流中发射元素的代码。
- conflate(),合并发射项,不对每个值进行处理。
- collectLatest(),取消并重新发射最后一个值。
- 当必须更改 CoroutineDispatcher 时,flowOn 操作符使用了相同的缓冲机制,但是 buffer 函数显式地请求缓冲而不改变执行上下文。
末端操作符
末端操作符是在流上用于启动流收集的挂起函数,collect 是最基础的末端操作符,但是还有另外一些更方便使用的末端操作符:
- 转化为各种集合,例如 toList 与 toSet
- 获取第一个(first)值与确保流发射单个(single)值的操作符
- 使用 reduce 与 fold 将流规约到单个值
组合多个流
就像 Kotlin 标准库中的 Sequence.zip 扩展函数一样,流拥有一个 zip 操作符用于组合两个流中的相关值。
展平流
流表示异步接收的值序列,所以很容易遇到这样的情况:每个值都会触发对另一个值序列的请求,然而,由于流具有异步的性质,因此需要不同的展平模式。
- flatMapConcat:连接模式
- flatMapMerge:合并模式
- flatMapLastest:最新展平模式
流的异常处理
当运算符中的发射器或代码抛出异常时
- try/catch 块
- catch 函数,处理上游异常
流的完成
当流收集完成时(普通情况或异常情况)它可能需要执行一个动作
- 命令式 finally 块
- onCompletion 声明式处理,可以获取到异常信息