前言
公司开启新项目了,想着准备亮一手 Kotlin 协程应用到项目中去,之前有对 Kotlin 协程的知识进行一定量的学习,以为自己理解协程了,结果……实在拿不出手!

为了更好的加深记忆和理解,更全面系统深入地学习 Kotlin 协程的知识,协程将分为三部分来讲解,本文是第一篇:
Kotlin 协程实战进阶(基础篇)
Kotlin 协程实战进阶(进阶篇)
Kotlin 协程实战进阶(高级篇)
本文大纲
一、概述
协程的概念在1958年就开始出现(比线程还早), 目前很多语言开始原生支, Java 没有原生协程但是大型公司都自己或者使用第三方库来支持协程编程, 但是Kotlin原生支持协程。
协程概念
很多人都会问协程是什么?这里引用官方的解释:
1.协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。
2.协程是一种并发设计模式。
协程就像轻量级的线程,为什么是轻量的?因为协程是依赖于线程,一个线程中可以创建N个协程,很重要的一点就是协程挂起时不会阻塞线程,几乎是无代价的。而且它基于线程池API,所以在处理并发任务这件事上它真的游刃有余。
协程只是一种概念,它提供了一种避免阻塞线程并用更简单、更可控的操作替代线程阻塞的方法:协程挂起和恢复。本质上Kotlin协程就是作为在Kotlin语言上进行异步编程的解决方案,处理异步代码的方法。
有可能有的同学问了,既然它基于线程池,那我直接使用线程池或者使用 Android 中其他的异步任务解决方案,比如 Handler、AsyncTask、RxJava等,不更好吗?
协程可以使用阻塞的方式写出非阻塞式的代码,解决并发中常见的回调地狱。消除了并发任务之间的协作的难度,协程可以让我们轻松地写出复杂的并发代码。一些本来不可能实现的并发任务变的可能,甚至简单,这些才是协程的优势所在。
作用
- 1.协程可以让异步代码同步化;
- 2.协程可以降低异步程序的设计复杂度。
特点
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
- 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
- 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
Kotlin Coroutine 生态

kotlin的协程实现分为了两个层次:
- 基础设施层:标准库的协程API,主要对协程提供了概念和语义上最基本的支持;
- 业务框架层 kotlin.coroutines:协程的上层框架支持,基于标准库实现的封装,也是我们日常开发使用的协程扩展库。
依赖库
在 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"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
}
这里我们主要使用协程扩展库, kotlin协程标准库太过于简陋不适用于开发者使用。
二、原理
协程的概念最核心的点就是函数或者一段程序能够被挂起,稍后再在挂起的位置恢复。协程通过主动让出运行权来实现协作,程序自己处理挂起和恢复来实现程序执行流程的协作调度。因此它本质上就是在讨论程序控制流程的机制。
使用场景
kotlin协程基于Thread相关API的封装,让我们不用过多关心线程也可以方便地写出并发操作,这就是Kotlin的协程。协程的好处本质上和其他线程api一样,方便。
在 Android 平台上,协程有两个主要使用场景:
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)
}
})
很明显我们需要处理很多的回调分支,如果业务多则更容易陷入「回调地狱」繁琐凌乱的代码中。
使用协程,同样可以像 Rx 那样有效地消除回调地狱,不过无论是设计理念,还是代码风格,两者是有很大区别的,协程在写法上和普通的顺序代码类似,同步的方式去编写异步执行的代码。使用协程改造后代码如下:
GlobalScope.launch(Dispatchers.Main) {
val result = userApi.getUserSuspend("suming")
tv_name.text = result?.name
}
这就是kotlin最有名的【非阻塞式挂起】,使用同步的方式完成异步任务,而且很简洁,这是Kotlin协程的魅力所在。之所有可以用看起来同步的方式写异步代码,关键在于请求函数getUserSuspend() 是一个挂起函数,被suspend 关键字修饰,下面会介绍。
 在上面的协程的原理图解中,耗时阻塞的操作并没有减少,只是交给了其他线程。userApi.getUserSuspend("suming") 真正执行的时候会切换到IO线程中执行,获取结果后最后恢复到主线程上,然后继续执行剩下的流程。
将业务流程原理拆分得更细致一点,在主线程中创建协程A 中执行整个业务流程,如果遇到异步调用任务则协程A 被挂起,切换到IO线程中创建子协程B ,获取结果后再恢复到主线程的协程A 上,然后继续执行剩下的流程。

协程Coroutine虽然不能脱离线程而运行,但可以在不同的线程之间切换,而且一个线程上可以一个或多个协程。下图动态显示了进程 - 线程 - 协程微妙关系。

此动图来源
三、基础
GlobalScope.launch(Dispatchers.Main) {
val result = userApi.getUserSuspend("suming")
tv_name.text = result?.name
}
上面就是启动协程的代码,启动协程的代码可以分为三部分:GlobalScope 、launch 、Dispatchers ,它们分别对应:协程的作用域、构建器和调度器。
1.协程的构建
上面的GlobalScope.launch() 属于协程构建器Coroutine builders ,Kotlin 中还有其他几种 Builders,负责创建协程:
runBlocking:T :顶层函数,创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,返回值是泛型T ,一般在项目中不会使用,主要是为main函数和测试设计的。launch :?创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。它返回的是一个该协程任务的引用,即Job 对象。这是最常用的用于启动协程的方式。async :?创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。并返回Deffer 对象,可通过调用Deffer.await() 方法等待该子协程执行完成并获取结果。常用于并发执行-同步等待和获取返回值的情况。
runBlocking
fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
- context:??协程的上下文,表示协程的运行环境,包括协程调度器、代表协程本身的Job、协程名称、协程ID等,默认值是当前线程上的事件循环。(这里的
context 和Android的context 不同,后面会讲解到) - block:???协程执行体,是一个用suspend关键字修饰的一个无参,无返回值的函数类型。是一个带接收者的函数字面量,接收者是CoroutineScope,因此执行体包含了一个隐式的
CoroutineScope ,所以在 runBlocking 内部可以来直接启动协程。 - T:?????返回值是泛型
T ,协程体block 中最后一行返回的是什么类型T 就是什么类型。
它是一个顶层函数,不是GlobalScope 的 API,可以在任意地方独立使用。它能创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,它的目的是将常规的阻塞代码与以挂起suspend 风格编写的库连接起来,常用于main 函数和测试中。一般我们在项目中是不会使用的。
fun runBloTest() {
print("start")
runBlocking {
delay(1000)
print("runBlocking")
}
print("end")
}
打印数据如下:

只有在runBlocking 协程体逻辑全部运行结束后,声明在runBlocking 之后的代码才能执行,即runBlocking 会阻塞其所在线程。
注意:runBlocking 虽然会阻塞当前线程的,但其内部运行的协程又是非阻塞的。
launch
launch 是最常用的用于启动协程的方式,用于在不阻塞当前线程的情况下启动一个协程,并返回对该协程任务的引用,即Job 对象。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
- context:?协程的上下文,表示协程的运行环境,包括协程调度器、代表协程本身的Job、协程名称、协程ID等,默认值是当前线程上的事件循环。
- start: ??协程启动模式,这些启动模式的设计主要是为了应对某些特殊的场景。业务开发实践中通常使用DEFAULT和LAZY这两个启动模式就够了。
- block:??协程代码,它将在提供的范围的上下文中被调用。它是一个用
suspend (挂起函数)关键字修饰的一个无参,无返回值的函数类型。接收者是CoroutineScope 的函数字面量。 - Job:???协程构建函数的返回值,可以把
Job 看成协程对象本身,封装了协程中需要执行的代码逻辑,是协程的唯一标识,Job可以取消,并且负责管理协程的生命周期。
协程需要运行在协程上下文环境中(即协程作用域,下面会讲解到),在非协程环境中launch 有两种方式创建协程:
GlobalScope.launch()
在应用范围内启动一个新协程,不会阻塞调用线程,协程的生命周期与应用程序一致。表示一个不绑定任何Job 的全局作用域,用于启动顶层协程,这些协程在整个应用程序生命周期中运行,不会提前取消(不存在Job )。
fun launchTest() {
print("start")
GlobalScope.launch {
delay(1000)
print("GlobalScope.launch")
}
print("end")
}
GlobalScope.launch() 协程将线程的执行权交出去,该线程继续干它要干的事情,主线程继续,而协程被延迟,到时间后会恢复至此继续向下执行。
打印数据如下:

由于这样启动的协程存在组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,尤其是在 Android 客户端这种需要频繁创建销毁组件的场景,因此不推荐这种用法。
注意:这里说的是GlobalScope 没有Job , 但是启动的launch 是有Job 的。 GlobalScope 本身就是一个作用域, launch 属于其子作用域。
CoroutineScope.launch()
启动一个新的协程而不阻塞当前线程,并返回对协程的引用作为一个Job 。通过CoroutineContext 至少一个协程上下文参数创建一个 CoroutineScope 对象。协程上下文控制协程生命周期和线程调度,使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁,从而实现安全可靠地协程调用。这是在应用中最推荐使用的协程使用方式。
fun launchTest2() {
print("start")
val job = CoroutineScope(Dispatchers.IO).launch {
delay(1000)
print("CoroutineScope.launch")
}
print("end")
}
打印数据如下:

launch 创建子协程
通过launch 在一个协程中启动子协程,可以根据业务需求创建一个或多个子协程:
fun launchTest3() {
print("start")
GlobalScope.launch {
delay(1000)
print("CoroutineScope.launch")
launch {
delay(1500)
print("launch 子协程")
}
}
print("end")
}
打印数据如下:

async
async 类似于launch ,都是创建一个不会阻塞当前线程的新的协程。它们区别在于:async 的返回是Deferred 对象,可通过Deffer.await() 等待协程执行完成并获取结果,而 launch 不行。常用于并发执行-同步等待和获取返回值的情况。
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
- context:?协程的上下文,同
launch 。 - start: ??协程启动模式,同
launch 。 - block:??协程代码,同
launch 。 - Deferred:?协程构建函数的返回值,继承自
Job ,一个有结果的Job ,可通过Deffer.await() 等待协程执行完成并获取结果。
await 获取返回值
fun asyncTest1() {
print("start")
GlobalScope.launch {
val deferred: Deferred<String> = async {
delay(2000)
print("asyncOne")
"HelloWord"
}
val result = deferred.await()
print("result == $result")
}
print("end")
}
上面例子中返回对象Deferred , 通过函数await() 获取结果值。打印数据如下:

注意:await() 不能在协程之外调用,因为它需要挂起直到计算完成,而且只有协程可以以非阻塞的方式挂起。所以把它放到协程中。
async 并发
当在协程作用域中使用async 函数时可以创建并发任务:
fun asyncTest2() {
print("start")
GlobalScope.launch {
val time = measureTimeMillis {
val deferredOne: Deferred<Int> = async {
delay(2000)
print("asyncOne")
100
}
val deferredTwo: Deferred<Int> = async {
delay(3000)
print("asyncTwo")
200
}
val deferredThr: Deferred<Int> = async {
delay(4000)
print("asyncThr")
300
}
val result = deferredOne.await() + deferredTwo.await() + deferredThr.await()
print("result == $result")
}
print("耗时 $time ms")
}
print("end")
}
打印数据如下:
 上面的代码就是一个简单的并发示例,async 是不阻塞线程的,也就是说上面三个async{} 异步任务是同时进行的。通过await() 方法可以拿到async 协程的执行结果,可以看到两个协程的总耗时是远少于9秒的,总耗时基本等于耗时最长的协程。
1.Deferred 集合还可以使用awaitAll() 等待全部完成;
2.如果Deferred 不执行await() 则async 内部抛出的异常不会被logCat 或tryCatch 捕获, 但是依然会导致作用域取消和异常崩溃; 但当执行await时异常信息会重新抛出。
3.惰性并发,如果将async 函数中的启动模式设置为CoroutineStart.LAZY 懒加载模式时则只有调用Deferred 对象的await 时(或者执行async.satrt() )才会开始执行异步任务。
launch 构建器适合执行 “一劳永逸” 的工作,意思就是说它可以启动新协程而不需要结果返回;async 构建器可启动新协程并允许您使用一个名为await 的挂起函数返回result ,并且支持并发。另外launch 和async 之间的很大差异是它们对异常的处理方式不同。如果使用async 作为最外层协程的开启方式,它期望最终是通过调用 await 来获取结果 (或者异常),所以默认情况下它不会抛出异常。这意味着如果使用 async 启动新的最外层协程,而不使用await ,它会静默地将异常丢弃。
2.Job & Deferred
反观线程,java平台上很明确地给出了线程的类型Thread ,我们也需要一个这样的类来描述协程,它就是Job 。它的API设计与Java的Thread 殊途同归。
Job
Job 是协程的句柄。如果把门和门把手比作协程和Job 之间的关系,那么协程就是这扇门,Job 就是门把手。意思就是可以通过Job 实现对协程的控制和管理。
从上面可以知道Job 是launch 构建协程返回的一个协程任务,完成时是没有返回值的。可以把Job 看成协程对象本身,封装了协程中需要执行的代码逻辑,协程的操作方法都在Job 身上。Job 具有生命周期并且可以取消,它也是上下文元素,继承自CoroutineContext 。
这里列举Job 几个比较有用的函数:
public interface Job : CoroutineContext.Element {
public val isActive: Boolean
public fun start(): Boolean
public fun cancel(cause: CancellationException? = null)
public suspend fun join()
public suspend fun Job.cancelAndJoin()
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
}
与Thread 相比,Job 同样有join() ,调用时会挂起(线程的join() 则会阻塞线程),直到协程完成;它的cancel() 可以类比Thread 的interrupt() ,用于取消协程;isActive 则是可以类比Thread 的isAlive() ,用于查询协程是否仍在执行。
Job 是一个接口类型,它具有以下三种状态:
状态 | 说明 |
---|
isActive | 活跃的。当Job 处于活动状态时为true ,如果Job 已经开始,但还没有完成、也没有取消或者失败,则是处于active 状态。 | isCompleted | 已完成。当Job 由于任何原因完成时为true ,已取消、已失败和已完成Job 都是被视为完成状态。 | isCancelled | 已退出。当Job 由于任何原因被取消时为true ,无论是通过显式调用cancel 或这因为它已经失败亦或者它的子或父被取消,都是被视为已退出状态。 |
这里模拟一个无限循环的协程,当协程是活跃状态时每秒钟打印两次消息,1.2秒后取消协程:
fun jobTest() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default){
var nextPrintTime = startTime
var i = 0
while (isActive) {
if (System.currentTimeMillis() >= nextPrintTime) {
print("job: I'm sleeping ${i++} ...")
nextPrintTime += 500
}
}
}
delay(1200)
print("等待1.2秒后")
job.cancelAndJoin()
print("协程被取消并等待完成")
}
join() 是一个挂起函数,它需要等待协程的执行,如果协程尚未完成,join() 立即挂起,直到协程完成;如果协程已经完成,join() 不会挂起,而是立即返回。打印数据如下:

Job 还可以有层级关系,一个Job 可以包含多个子Job ,当父Job 被取消后,所有的子Job 也会被自动取消;当子Job 被取消或者出现异常后父Job 也会被取消。具有多个子 Job 的父Job 会等待所有子Job 完成(或者取消)后,自己才会执行完成。
总的来说:它的作用是Job 实例作为协程的唯一标识,用于处理协程,并且负责管理协程的生命周期。
Deferred
Deferred 继承自Job ,具有与Job 相同的状态机制。它是async 构建协程返回的一个协程任务,可通过调用await() 方法等待协程执行完成并获取结果。不同的是Job 没有结果值,Deffer 有结果值。
public interface Deferred<out T> : Job {
public suspend fun await(): T
}
await() :?等待协程执行完毕并返回结果,如果异常结束则会抛出异常;如果协程尚未完成,则挂起直到协程执行完成。T :????这里多了一个泛型参数T ,它表示返回值类型,通过await() 函数可以拿到这个返回值。
上面已有Deferred 代码演示,这里就不再重复实践。
3.作用域
通常我们提到的域 ,都是用来描述范围的,域 既有约束作用又有提供额外能力的作用。
协程作用域(CoroutineScope )其实就是为协程定义的作用范围,为了确保所有的协程都会被追踪,Kotlin 不允许在没有使用CoroutineScope 的情况下启动新的协程。CoroutineScope 可被看作是一个具有超能力的ExecutorService 的轻量级版本。它能启动新的协程,同时这个协程还具备上面所说的suspend 和resume 的优势。
每个协程生成器launch 、async 等都是CoroutineScope 的扩展,并继承了它的coroutineContext 自动传播其所有元素和取消。协程作用域本质是一个接口:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
因为启动协程需要作用域,但是作用域又是在协程创建过程中产生的,这似乎是一个“先有鸡后有蛋还是先有蛋后有鸡”的问题。
常用作用域
官方库给我们提供了一些作用域可以直接来使用:
runBlocking :顶层函数,它的第二个参数为接收者是CoroutineScope 的函数字面量,可启动协程。但是它会阻塞当前线程,主要用于测试。GlobalScope :全局协程作用域,通过GlobalScope 创建的协程不会有父协程,可以把它称为根协程 。它启动的协程的生命周期只受整个应用程序的生命周期的限制,且不能取消,在运行时会消耗一些内存资源,这可能会导致内存泄露,所以仍不适用于业务开发。coroutineScope :创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。它是一个挂起函数,需要运行在协程内或挂起函数内。当这个作用域中的任何一个子协程失败时,这个作用域失败,所有其他的子程序都被取消。为并行分解工作而设计的。supervisorScope :与coroutineScope 类似,不同的是子协程的异常不会影响父协程,也不会影响其他子协程。(作用域本身的失败(在block 或取消中抛出异常)会导致作用域及其所有子协程失败,但不会取消父协程。)MainScope :为UI组件创建主作用域。一个顶层函数,上下文是SupervisorJob() + Dispatchers.Main ,说明它是一个在主线程执行的协程作用域,通过cancel 对协程进行取消。推荐使用。
fun scopeTest() {
GlobalScope.launch {
launch {
print("GlobalScope的子协程")
}
launch {
print("GlobalScope的第二个子协程")
}
}
val mainScope = MainScope()
mainScope.launch {
}
}
注意:MainScope 作用域的好处就是方便地绑定到UI组件的声明周期上,在Activity销毁的时候mainScope.cancel() 取消其作用域。
Lifecycle的协程支持
Android 官方对协程的支持是非常友好的,KTX 为 Jetpack 的Lifecycle 相关组件提供了已经绑定UV声明周期的作用域供我们直接使用:
lifecycleScope :Lifecycle Ktx 库提供的具有生命周期感知的协程作用域,与Lifecycle 绑定生命周期,生命周期被销毁时,此作用域将被取消。会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,推荐使用。viewModelScope :与lifecycleScope 类似,与ViewModel 绑定生命周期,当ViewModel 被清除时,这个作用域将被取消。推荐使用。
在build.gradle 添加Lifecycle相应基础组件后,再添加以下组件即可:
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
因为Activity 实现了LifecycleOwner 这个接口,而lifecycleScope 则正是它的拓展成员,可以在Activity中直接使用lifecycleScope 协程实例:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn_data.setOnClickListener {
lifecycleScope.launch {
}
}
}
}
在ViewModel 中使用创建协程:
class MainViewModel : ViewModel() {
fun getData() {
viewModelScope.launch {
}
}
}
注意:VIewModel 的作用域会在它的 clear 函数调用时取消。
分类和行为规则
官方框架在实现复合协程的过程中也提供了作用域,主要用于明确父子关系,以及取消或者异常处理等方面的传播行为。该作用域分为以下三种:
- 顶级作用域:没有父协程的协程所在的作用域为顶级作用域。
- 协同作用域:协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。
- 主从作用域:与协同作用域在协程的父子关系上一致,区别在于,处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程。
除了三种作用域中提到的行为以外,父子协程之间还存在以下规则:
- 父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。
- 父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。
- 子协程会继承父协程的协程上下文中的元素,如果自身有相同
key 的成员,则覆盖对应的key ,覆盖的效果仅限自身范围内有效。
4.调度器
在上面介绍协程概念的时候,协程的挂起与恢复在哪挂起,什么时候恢复,为什么能切换线程,这因为调度器的作用:它确定相应的协程使用那些线程来执行。
CoroutineDispatcher 调度器指定指定执行协程的目标载体,它确定了相关的协程在哪个线程或哪些线程上执行。可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
协程需要调度的位置就是挂起点的位置,只有当挂起点正在挂起的时候才会进行调度,实现调度需要使用协程的拦截器。调度的本质就是解决挂起点恢复之后的协程逻辑在哪里运行的问题。调度器也属于协程上下文一类,它继承自拦截器:
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
public abstract fun dispatch(context: CoroutineContext, block: Runnable)
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
public operator fun plus(other: CoroutineDispatcher): CoroutineDispatcher = other
}
它是所有协程调度程序实现扩展的基类(我们很少会自己自定义调度器)。可以使用newSingleThreadContext 和newFixedThreadPoolContext 创建私有线程池。也可以使用asCoroutineDispatcher 扩展函数将任意java.util.concurrent.Executor 转换为调度程序。
调度器模式
主要的调度器类型有:
调度器模式 | 说明 | 适用场景 |
---|
Dispatchers.Default | 默认调度器,CPU 密集型任务调度器,适合处理后台计算。 | 通常处理一些单纯的计算任务,或者执行时间较短任务比如:Json 的解析,数据计算等。 | Dispatchers.Main | UI 调度器, 即在主线程上执行。 | 调度程序是单线程的,通常用于UI 交互,刷新等。 | Dispatchers.Unconfined | 一个不局限于任何特定线程的协程调度程序,即非受限调度器。 | 子协程切换线程代码会运行在原来的线程上,协程在相应的挂起函数使用的任何线程中继续。 | Dispatchers.IO | IO 调度器,执行的线程是IO 线程。 | 适合执行IO 相关操作,比如:网络处理,数据库操作,文件读写等。 |
所有的协程构造器(如launch 和async )都接受一个可选参数,即 CoroutineContext ,该参数可用于显式指定要创建的协程和其它上下文元素所要使用的CoroutineDispatcher 。
fun dispatchersTest() {
val mainScope = MainScope()
mainScope.launch {
launch(Dispatchers.Main) {
print("主线程调度器")
}
launch(Dispatchers.Default) {
print("默认调度器")
}
launch(Dispatchers.Unconfined) {
print("任意调度器")
}
launch(Dispatchers.IO) {
print("IO调度器")
}
}
}
打印数据如下:

withContext
在 Andorid 开发中,我们常常在子线程中请求网络获取数据,然后切换到主线程更新UI。官方为我们提供了一个withContext 顶级函数,使用withContext 函数来改变协程的上下文,而仍然驻留在相同的协程中。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
- context:?协程的上下文,同上(调度器也属于上下文一类)。
- block:??协程执行体,同上。
block 中的代码会被调度到上面指定的调度器上执行,并返回结果值。
这个函数会使用新指定的上下文的dispatcher ,将block 的执行转移到指定的线程中。它会返回结果, 可以和当前协程的父协程存在交互关系, 主要作用为了来回切换调度器。
GlobalScope.launch(Dispatchers.Main) {
val result: User = withContext(Dispatchers.IO) {
userApi.getUserSuspend("FollowExcellence")
}
tv_title.text = result.name
}
在主线程中启动一个协程,然后再通过withContext(Dispatchers.IO) 调度到IO 线程上去做网络请求,把获取到的结果返回,然后再在主线程中完成UI的更新。
注意:与async{}.await() 相比,withContext 的内存开销更低,对于使用async 之后立即调用await() 的情况,应优先使用withContext 。
5.协程上下文
CoroutineContext 表示协程上下文,是 Kotlin 协程的一个基本结构单元。协程上下文主要承载着资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。它有很多作用,包括携带参数,拦截协程执行等等。如何运用协程上下文是至关重要的,以此来实现正确的线程行为、生命周期、异常以及调试。
协程上下文的数据结构特征更加显著,与List和Map非常类似。它包含用户定义的一些数据集合,这些数据与协程密切相关。它是一个有索引的 Element 实例集合。每个 element 在这个集合有一个唯一的Key 。
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E?
}
}
<E> get(key) : ?可以通过key 从这个上下文中获取这个Element 元素或者null 。fold() : ????提供遍历当前上下文中所有元素的能力。plus(context) :?顾名思义它是一个加法运算,多个上下文元素可以通过+ 的形式整合成一个上下文返回。minusKey(key) :?与plus 相反,减法运算,删除当前上下文中指定key 的元素,返回的是不包含指定Element :????协程上下文的一个元素,本身就是一个单例上下文,里面有一个key ,是这个元素的索引。
Element 本身也实现了CoroutineContext 接口,像Int 实现了List<Int> 一样,为什么元素本身也是集合呢?主要是Element 它不会存放除它自己以外的数据;Element 属性又有一个key ,是协程上下文这个集合中元素的索引。这个索引在元素里面,说明元素一产生就找到自己的位置。
注意:协程上下文的内部实现实际是一个单链表。
协程使用以下几种元素集定义协程的行为,它们均继承自CoroutineContext :
Job :??????????协程的句柄,对协程的控制和管理生命周期。CoroutineName :??????协程的名称,可用于调试。CoroutineDispatcher :???调度器,确定协程在指定的线程来执行。CoroutineExceptionHandler :协程异常处理器,处理未捕获的异常。这里暂不做深入分析,后面的文章会讲解到,敬请期待。
CoroutineName
public data class CoroutineName(
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
public companion object Key : CoroutineContext.Key<CoroutineName>
}
CoroutineName 是用户用来指定的协程名称的,用于方便调试和定位问题:
GlobalScope.launch(CoroutineName("GlobalScope")) {
launch(CoroutineName("CoroutineA")) {
val coroutineName = coroutineContext[CoroutineName]
print(coroutineName)
}
}
协程内部可以通过coroutineContext 这个全局属性直接获取当前协程的上下文。打印数据如下:
[DefaultDispatcher-worker-2] CoroutineName(CoroutineA)
上下文组合
从上面的协程创建的函数中可以看到,协程上下文的参数只有一个,但是怎么传递多个上下文元素呢?如果需要为协程上下文定义多个元素,那就可以用 + 运算符:
GlobalScope.launch {
var context = CoroutineName("协程1") + Dispatchers.Main
print("context == $context")
context += Dispatchers.IO
print("context == $context")
val contextResult = context.minusKey(context[CoroutineName]!!.key)
print("contextResult == $contextResult")
}
注意:如果有重复的元素(key 一致)则会右边的会代替左边的元素。打印数据如下:
context == [CoroutineName(协程1), Dispatchers.Main]
context == [CoroutineName(协程1), Dispatchers.IO]
contextResult == Dispatchers.IO
6.suspend 挂起函数
suspend 是 Kotlin 协程最核心的关键字,使用suspend 关键字修饰的函数叫作挂起函数 ,挂起函数 只能在协程体内或者在其他挂起函数 内调用。否则 IDE 就会提示一个错误:
Suspend function ‘xxxx’ should be called only from a coroutine or another suspend function
协程提供了一种避免阻塞线程并用更简单、更可控的操作替代线程阻塞的方法:协程挂起和恢复。协程在执行到有suspend 标记的函数时,当前函数会被挂起,让出运行权,直到该挂起函数内部逻辑完成,才会在挂起的地方resume 恢复继续执行。
本质上,挂起函数就是一个提醒作用,函数创建者给函数调用者的提醒,表示这是一个(耗时)任务,被创建者用挂起的方式放在后台运行。调用者需要在协程里调用。协程通过挂起函数这种形式,它把(耗时)任务切线程这个工作,实际上交给了函数的创建者而不是调用者。对于调用者非常简单,只会收到一个提醒,只需把挂起函数放在协程里面,剩下交给协程来处理。
挂起点
协程内部挂起函数调用的地方称为挂起点,或者有下面这个标识的表示这个就是挂起点。

挂起和恢复
Kotlin 协程中挂起点如果出现异步调用,那么当前协程就会被挂起,挂起时就将挂起点的信息保存到Continuation 对象中,它携带了协程继续执行所需要的上下文数据,直到对应的Continuation 通过调用resumeWith 函数才会恢复协程的执行,同时返回Result<T> 类型的成功或者异常的结果。
- 挂起:挂起函数调用时则包含了协程“挂起”的语义,挂起函数调用的时候,主调用流程就挂起了。
- 恢复:挂起函数返回时则包含了协程“恢复”的语义,挂起函数返回时,意味着主调用流程就要恢复执行了。
Kotlin 协程的挂起和恢复能力本质上就是挂起函数的挂起和恢复。协程挂起之后需要恢复resume 的,也就是把线程切回来,而恢复的这个功能也是协程的,所以如果一个挂起函数不在协程里面被调用的话,那么这个恢复的功能就没法实现了。
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
fun <T> Continuation<T>.resume(value: T): Unit =
resumeWith(Result.success(value))
fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
resumeWith(Result.failure(exception))
Kotlin 的 Continuation 类有一个 resumeWith 函数可以接收 Result 类型的参数。在结果成功获取时,调用resumeWith(Result.success(value)) 或者调用拓展函数resume(value) ;出现异常时,调用resumeWith(Result.failure(exception)) 或者调用拓展函数resumeWithException(exception) ,这就是 Continuation 的恢复调用。
Continuation 类似于网络请求回调Callback ,也是一个请求成功和一个请求失败的回调:
public interface Callback {
void onFailure(Call call, IOException e);
void onResponse(Call call, Response response) throws IOException;
}
注意:suspend 不一定真的会挂起,如果只是提供了挂起的条件,但是协程没有产生异步调用,那么协程还是不会被挂起。
协程在常规函数的基础上添加了suspend 和 resume 两项操作用于处理长时间运行的任务:
suspend :用于挂起执行当前协程,并保存所有局部变量。resume :用于让已挂起的协程从挂起处恢复继续执行。
那么协程是如何做到挂起和恢复?
suspend本质(夺命七步)
一个挂起函数 要挂起,那么它必定得有一个挂起点 ,不然无法知道函数是否挂起,从哪挂起呢?
@GET("users/{login}")
suspend fun getUserSuspend(@Path("login") login: String): User
第一步:将上面的挂起函数解析成字节码:通过AS的工具栏中Tools ->kotlin ->show kotlin ByteCode
public abstract getUserSuspend(Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
上面的挂起函数本质是这样的,你会发现多了一个参数,这个参数就是Continuation ,也就是说调用挂起函数的时候需要传递一个Continuation 给它,只是传递这个参数是由编译器悄悄传,而不是我们传递的。这就是挂起函数为什么只能在协程或者其他挂起函数中执行,因为只有挂起函数或者协程中才有Continuation 。
第二步:这里的Continuation 参数,其实它类似CallBack 回调函数,resumeWith() 就是成功或者失败回调的结果:
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
第三步:但是它是从哪里传进来的呢?这个函数只能在协程或者挂起函数中执行,说明Continuation 很有可能是从协程充传入来的,查看协程构建的源码:
public fun CoroutineScope.launch(): 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
}
第四步:通过launch 启动一个协程的时候,他通过coroutine 的start 方法启动协程:
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
initParentJob()
start(block, receiver, this)
}
第五步:然后start 方法里面调用了CoroutineStart 的invoke ,这个时候我们发现了Continuation :
public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(completion)
ATOMIC -> block.startCoroutine(completion)
UNDISPATCHED -> block.startCoroutineUndispatched(completion)
LAZY -> Unit
}
第六步:而 Continuation 通过block.startCoroutine(completion) 传入:
public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) {
createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}
第七步:最终回调到上面Continuation 的resumeWith() 恢复函数里面。这里可以看出协程体本身就是一个Continuation ,这也就解释了为什么必须要在协程内调用suspend 挂起函数了。(由于篇幅原因这里不做深入分析,后续的文章会分析这里,敬请期待!)
额外知识点:在创建协程的底层源码中,创建协程会返回一个Continuation 实例,这个实例就是套了几层马甲的协程体,调用它的resume 可以触发协程的执行。
任何一个协程体或者挂起函数中都隐含有一个Continuation 实例,编译器能够对这个实例进行正确的传递,并将这个细节隐藏在协程的背后,让我们的异步代码看起来像同步代码一样。
@GET("users/{login}")
suspend fun getUserSuspend(@Path("login") login: String): User
GlobalScope.launch(Dispatchers.Main) {
val result = userApi.getUserSuspend("suming")
tv_name.text = result?.name
}
launch() 创建的这个协程,在执行到某一个suspend 挂起函数的时候,这个协程会被挂起,从当前线程挂起。 也就是说这个协程从正在执行它的线程上脱离,这个协程在挂起函数指定的线程上继续执行,当协程的任务完成时,再resume 恢复切换到原来的线程上继续执行。
在主线程进行的暂停协程和恢复协程的两个操作,既实现了将耗时任务交由后台线程完成,保障了主线程安全,又以同步代码的方式完成了实际上的多线程异步调用。可以说,在 Android 平台上协程主要就用来解决异步和切换线程这两个问题。
点关注,不迷路
好了各位,以上就是这篇文章的全部内容了,很感谢您阅读这篇文章。我是suming,感谢各位的支持和认可,您的点赞就是我创作的最大动力。山水有相逢,我们下篇文章见!
本人水平有限,文章难免会有错误,请批评指正,不胜感激 !
参考链接:
希望我们能成为朋友,在 Github、 博客 上一起分享知识,一起共勉!Keep Moving!
|