## 参考文章
[Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了](https://kaixue.io/kotlin-coroutines-2/)
## 上期回顾
大部分情况下,我们都是用`launch`函数来创建协程,其实还有其他两个函数也可以用来创建协程:
* `runBlocking`
* `async`
`runBlocking`通常适用于单元测试的场景,而业务开发中不会用到这个函数,因为它是线程阻塞的。
接下来我们主要来对比`launch`与`async`这两个函数。
* 相同点:它们都可以用来启动一个协程,返回的都是`Coroutine`,我们这里不需要纠结具体是返回哪个类。
* 不同点:`async`返回的`Coroutine`多实现了`Deferred`接口。
关于`Deferred`更深入的知识就不在这里过多阐述,它的意思就是延迟,也就是结果稍后才能拿到。
我们调用`Deferred.await()`就可以得到结果了。
接下来我们继续看看`async`是如何使用的,先回忆一下上期中获取头像的场景:
~~~kotlin
coroutineScope.launch(Dispatchers.Main) {
// 👇 async 函数启动新的协程
val avatar: Deferred = async { api.getAvatar(user) } // 获取用户头像
val logo: Deferred = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
// 👇 👇 获取返回值
show(avatar.await(), logo.await()) // 更新 UI
}
~~~
可以看到 avatar 和 logo 的类型可以声明为`Deferred`,通过`await`获取结果并且更新到 UI 上显示。
`await`函数签名如下:
~~~kotlin
public suspend fun await(): T
~~~
前面有个关键字是之前没有见过的 ——`suspend`,这个关键字就对应了上期最后我们留下的一个问号:协程最核心的那个「非阻塞式」的「挂起」到底是怎么回事?
所以接下来,我们的核心内容就是来好好说一说这个「挂起」。
## 「挂起」的本质
协程中「挂起」的对象到底是什么?挂起线程,还是挂起函数?都不对,**我们挂起的对象是协程。**
还记得协程是什么吗?**启动一个协程可以使用`launch`或者`async`函数,协程其实就是这两个函数中闭包的代码块。**
`launch`,`async`或者其他函数创建的**协程,在执行到某一个`suspend`函数的时候,这个协程会被「suspend」,也就是被挂起。**
**那此时又是从哪里挂起?从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。**
>[success]注意,**不是这个协程停下来了!是脱离,当前线程(协程所在的线程)从这行代码开始不再运行这个协程了,不再管这个协程要去做什么了**。
suspend 是有暂停的意思,但我们在协程中应该理解为:**当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了**。
我们先让时间静止,然后兵分两路,分别看看这两个互相脱离的线程和协程接下来将会发生什么事情:
### **线程:**
前面我们提到,**挂起会让协程从正在执行它的线程上脱离**,具体到代码其实是:
**协程的代码块中,线程执行到了 suspend 函数这里的时候,就暂时不再执行剩余的协程代码,跳出协程的代码块。**
那线程接下来会做什么呢?该干嘛干嘛
如果它是一个后台线程:
* 要么无事可做,被系统回收
* 要么继续执行别的后台任务
**总之,跟 Java 线程池里的线程在工作结束之后是完全一样的:回收或者再利用。**
**如果这个线程它是 Android 的主线程,那它接下来就会继续回去工作:也就是一秒钟 60 次的界面刷新任务**。
什么是继续回去工作?示例如下
~~~kotlin
// 主线程中
GlobalScope.launch(Dispatchers.Main) {
val image = suspendingGetImage(imageId) // 获取图片
avatarIv.setImageBitmap(image) // 显示出来
}
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
//相当于:
handler.post {
val image = suspendingGetImage(imageId)
avatarIv.setImageBitmap(image)
}
~~~
首先,如果你启动一个执行主线程任务的协程,它实质上会往你的主线程post()一个新任务Runnable,这个任务Runnable就是你的协程代码需要完成的任务,那么当这个协程被挂起的时候,那实质上就是你post()的这个任务Runnable提前结束了。那这时候主线程干嘛呢?继续刷新界面呗。那剩下的代码怎么办?协程不是还没执行完么?刚才也说了,兵分两路。稍后看协程。
一个常见的场景是,获取一个图片,然后显示出来:
~~~kotlin
// 主线程中
GlobalScope.launch(Dispatchers.Main) {
val image = suspendingGetImage(imageId) // 获取图片
avatarIv.setImageBitmap(image) // 显示出来
}
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
~~~
这段执行在主线程的协程,它实质上会往你的主线程`post`一个`Runnable`,这个`Runnable`就是你的协程代码:
~~~kotlin
handler.post {
val image = suspendingGetImage(imageId)
avatarIv.setImageBitmap(image)
}
~~~
当这个协程被挂起的时候,就是主线程`post`的这个`Runnable`提前结束,然后继续执行它界面刷新的任务。
关于线程,我们就看完了。
这个时候你可能会有一个疑问,那`launch`包裹的剩下代码怎么办?协程不是还没执行完么?刚才也说了,兵分两路。稍后看协程。
所以接下来,我们来看看协程这一边。
### **协程:**
*****
线程的代码在到达`suspend`函数的时候被掐断,接下来协程会从这个`suspend`函数开始继续往下执行,不过是在**指定的线程**。
*****
**谁指定的?是`suspend`函数指定的,比如我们这个例子中,函数内部的`withContext`传入的`Dispatchers.IO`所指定的 IO 线程。** 另外在挂起函数执行完成之后,协程为我们做的最爽的事就来了,**它会自动帮我们把协程再切回来**。
#### **`Dispatchers`调度器小知识**
*****
`Dispatchers`调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行,关于`Dispatchers`这里先不展开了。
那我们平日里常用到的调度器有哪些?
常用的`Dispatchers`,有以下三种:
* `Dispatchers.Main`:Android 中的主线程
* `Dispatchers.IO`:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
* `Dispatchers.Default`:适合 CPU 密集型的任务,比如计算
*****
回到我们的协程,它从`suspend`函数开始脱离启动它的线程,继续执行在`Dispatchers`所指定的 IO 线程。
紧接着在`suspend`函数执行完成之后,协程为我们做的最爽的事就来了:会**自动帮我们把线程再切回来**。
这个「切回来」是什么意思?
我们的协程原本是运行在**主线程**的,当代码遇到 suspend 函数的时候,发生线程切换,根据`Dispatchers`切换到了 IO 线程;这个所谓的切回来就是:当这个挂起函数执行完毕后,协程会帮我再`post`一个`Runnable`任务,让我剩下的代码继续回到主线程去执行。这就是为啥你指定线程的那个参数不叫`Threads`,而是叫做`Dispatchers`调度器,它不只是只能指定协程执行的线程,还能在suspend挂起函数之后自动再切回来。其实,也不是一定会切回来,也可以通过设置特殊的`Dispatchers`来让挂起函数执行完之后也不切回来,不过这是你自己的选择,而不是它的定位。**挂起的定位就是暂时切走,稍后再切回来**。
我们从线程和协程的两个角度都分析完成后,终于可以对协程的「挂起」suspend 做一个解释:
**协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,其实个开启一个协程一样,说起来比较玄乎,但其实就是切个线程;**
不过区别在于,**挂起函数在执行完成之后,协程会重新切回它原先的线程**。
再简单来讲,在 Kotlin 中所谓的挂起,其实就是**一个稍后会被自动切回来的线程调度操作**。
>[success] 这个「切回来」的动作,在 Kotlin 里叫做[resume](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/-continuation/resume.html),恢复。
那么上期我们最后一个问题,**为什么挂起函数只能在协程里或者另一个挂起函数里面被调用?**
* 首先,通过刚才的分析我们知道:**挂起之后是需要恢复**。而**恢复这个功能是协程的**,如果你不在协程里面调用,恢复这个功能没法实现。
* 另外,再细想下这个逻辑:一个挂起函数要么在协程里被调用,要么在另一个挂起函数里被调用,那么它其实就是直接或者间接地,总是会在一个协程里被调用的。
所以,**要求`suspend`函数只能在协程里或者另一个 suspend 函数里被调用,还是为了要让协程能够在`suspend`函数切换线程之后再切回来**。
## 怎么就「挂起」了?
我们**先了解到了什么是「挂起」后,再接着看看这个「挂起」是怎么做到的**。
首先你可以写一个自定义的`suspend`函数,然后在主线程上的协程里去调用它,你会发现它还是运行在主线程,没有切换。
~~~kotlin
suspend fun suspendingPrint(Dispatchers.Main) {
println("Thread: ${Thread.currentThread().name}")
}
launch(){
suspendingPrint()
}
I/System.out: Thread: main
~~~
输出的结果还是在主线程。没有切换。
为什么没切换线程?因为它不知道往哪切,需要我们告诉它。
对比之前例子中`suspendingGetImage`函数代码:
~~~kotlin
// 👇
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
~~~
我们可以发现不同之处其实在于`withContext`函数。
其实通过`withContext`源码可以知道,它本身就是一个挂起函数,它接收一个`Dispatcher`参数,依赖这个`Dispatcher`参数的指示,你的协程被挂起,然后切到别的线程。
所以这个`suspend`,其实并不是起到把任何把协程挂起,或者说切换线程的作用。还需要你在挂起函数里面去调用另外一个挂起函数,而且里面这个挂起函数需要是协程自带的、内部实现了协程挂起代码的,或者它不是自带的,但它的内部直接或者间接地调用了某一个自带的挂起函数,这也是可以的,总之你最终需要调用到一个自带的挂起函数,让它来去真正做挂起,也就是线程切换的工作。
**真正挂起协程这件事,是 Kotlin 的协程框架帮我们做的**。也就是说**所谓的协程被挂起或者说切线程这件事,它并不是发生在你外部这个挂起函数被调用的时候,而是里面那个挂起函数,那个`withContext`函数被调用的时候**。
所以我们**想要自己写一个挂起函数,仅仅只加上`suspend`关键字是不行的,还需要函数内部直接或间接地调用到 Kotlin 协程框架自带的`suspend`函数(挂起)才行**。
>[info]备注:自带的挂起函数不只是`withContext()`一个,还有其他的,他们都能实现协程的挂起,而我们要想自己写一个自定义的挂起函数,就需要在这个自定义的挂起函数内部直接或者间接地去调用到某一个自带的挂起函数才行。
## suspend 的意义?
这个`suspend`关键字,**既然它并不是真正实现挂起,那它的作用是什么?**
**它其实是一个提醒。** 谁对谁的提醒?
**函数的创建者对函数的调用者的提醒:我是一个耗时函数,我被我的创建者用挂起的方式放在后台运行,所以请在协程里调用我。表面上它是一个要求,你需要在协程里调用我,但本质上,它其实是一个提醒——我是一个被自动放在后台运行的耗时函数,所以你需要在协程里调用我**
这个提醒又有什么作用呢?**它能让我们的主线程不卡**,对比我们在写Java代码时,在主线程做事需要非常小心,一不留神,我们在主线程调用了一个耗时方法,那就会卡一下,而且这种事情是很难避免的。我又不知道哪个方法会耗时?又不是我写的,就算是我写的,万一哪天给忘了呢?而**协程通过挂起函数这种方式,它把耗时任务切线程这个工作,实际上交给了函数的创建者,而不是调用者。对于调用者而言,事情非常简单,它只会收到一个提醒,你需要把我放在协程里面,剩下的其他调用者都不用管,而通过`suspend`关键字这种方式,它实际上作为一个提醒,是形成了一种机制,一种让所有耗时任务全都自动放在后台执行的机制,那么主线程是不是就不卡了,所以为什么`suspend`关键字并没有实际去操作挂起,但 Kotlin 却把它提供出来让我们使用?因为它本来就不是用来操作挂起的**。
**挂起的操作 —— 也就是切线程,依赖的是挂起函数里面的实际代码,而不是这个关键字**。
所以这个关键字,**只是一个提醒**。
还记得刚才我们尝试自定义挂起函数的方法吗?
~~~kotlin
// 👇 redundant suspend modifier
suspend fun suspendingPrint() {
println("Thread: ${Thread.currentThread().name}")
}
~~~
![](https://img.kancloud.cn/e1/be/e1be27eb1139c204f12a7d1d0a3a1801_865x91.png)
如果你创建一个`suspend`函数但它内部不包含真正的挂起逻辑,编译器会给你一个提醒:`redundant suspend modifier`,告诉你这个`suspend`是多余的。
因为你这个函数实质上并没有发生挂起,那你这个`suspend`关键字只有一个效果:就是限制这个函数只能在协程里被调用,如果在非协程的代码中调用,就会编译不通过。
所以,**创建一个`suspend`函数,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的`suspend`函数,你的这个`suspend`才是有意义的**。
## 怎么自定义 suspend 函数?
在了解了`suspend`关键字的来龙去脉之后,我们就可以进入下一个话题了:怎么自定义`suspend`函数。
这个「怎么自定义」其实分为两个问题:
* 什么时候需要自定义`suspend`函数?
* 原则:耗时
* 具体该怎么写呢?
### 什么时候需要自定义 suspend 函数
如果你的某个函数比较耗时,也就是要等的操作,那就把它写成`suspend`函数。这就是原则。
**耗时操作一般分为两类:I/O 操作和 CPU 计算工作。比如文件的读写、网络交互、图片的模糊处理,都是耗时的,通通可以把它们写进`suspend`函数里。**
**另外这个「耗时」还有一种特殊情况,就是这件事本身做起来并不慢,但它需要等待,比如 5 秒钟之后再做这个操作。这种也是`suspend`函数的应用场景**。
### 具体该怎么写
给函数加上`suspend`关键字,然后在`withContext`把函数的内容包住就可以了。
提到用`withContext`是因为它在挂起函数里功能最简单直接:把线程自动切走和切回。
当然并不是只有`withContext`这一个函数来辅助我们实现自定义的`suspend`函数,别的挂起函数功能总会比它多一些或者少一些,比如还有一个挂起函数叫`delay`,它的作用是等待一段时间后再继续往下执行代码。
使用它就可以实现刚才提到的等待类型的耗时操作:
~~~kotlin
suspend fun suspendUntilDone() {
while (!done) {
delay(5)
}
}
~~~
这些东西,在我们初步使用协程的时候不用立马接触,可以先把协程最基本的方法和概念理清楚。
## 总结
我们今天整个文章其实就在理清一个概念:什么是挂起?**挂起,就是一个稍后会被自动切回来的线程调度操作。**
好,关于协程中的「挂起」我们就解释到这里。
可能你心中还会存在一些疑惑:
* 协程中挂起的「非阻塞式」到底是怎么回事?
* 协程和 RxJava 在切换线程方面功能是一样的,都能让你写出避免嵌套回调的复杂并发代码,那协程还有哪些优势,或者让开发者使用协程的理由?
这些疑惑的答案,我们都会在下一篇中全部揭晓。
- 前言
- Kotlin简介
- IntelliJ IDEA技巧总结
- idea设置类注释和方法注释模板
- 像Android Studion一样创建工程
- Gradle
- Gradle入门
- Gradle进阶
- 使用Gradle创建一个Kotlin工程
- 环境搭建
- Androidstudio平台搭建
- Eclipse的Kotlin环境配置
- 使用IntelliJ IDEA
- Kotlin学习路线
- Kotlin官方中文版文档教程
- 概述
- kotlin用于服务器端开发
- kotlin用于Android开发
- kotlin用于JavaScript开发
- kotlin用于原生开发
- Kotlin 用于数据科学
- 协程
- 多平台
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 开始
- 基本语法
- 习惯用法
- 编码规范
- 基础
- 基本类型
- 包与导入
- 控制流
- 返回与跳转
- 类与对象
- 类与继承
- 属性与字段
- 接口
- 可见性修饰符
- 扩展
- 数据类
- 密封类
- 泛型
- 嵌套类
- 枚举类
- 对象
- 类型别名
- 内嵌类
- 委托
- 委托属性
- 函数与Lambda表达式
- 函数
- Lambda表达式
- 内联函数
- 集合
- 集合概述
- 构造集合
- 迭代器
- 区间与数列
- 序列
- 操作概述
- 转换
- 过滤
- 加减操作符
- 分组
- 取集合的一部分
- 取单个元素
- 排序
- 聚合操作
- 集合写操作
- List相关操作
- Set相关操作
- Map相关操作
- 多平台程序设计
- 平台相关声明
- 以Gradle创建
- 更多语言结构
- 解构声明
- 类型检测与转换
- This表达式
- 相等性
- 操作符重载
- 空安全
- 异常
- 注解
- 反射
- 作用域函数
- 类型安全的构造器
- Opt-in Requirements
- 核心库
- 标准库
- kotlin.test
- 参考
- 关键字与操作符
- 语法
- 编码风格约定
- Java互操作
- Kotlin中调用Java
- Java中调用Kotlin
- JavaScript
- 动态类型
- kotlin中调用JavaScript
- JavaScript中调用kotlin
- JavaScript模块
- JavaScript反射
- JavaScript DCE
- 原生
- 并发
- 不可变性
- kotlin库
- 平台库
- 与C语言互操作
- 与Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 调试
- FAQ
- 协程
- 协程指南
- 基础
- 取消与超时
- 组合挂起函数
- 协程上下文与调度器
- 异步流
- 通道
- 异常处理与监督
- 共享的可变状态与并发
- Select表达式(实验性)
- 工具
- 编写kotlin代码文档
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin与OSGI
- 编译器插件
- 编码规范
- 演进
- kotlin语言演进
- 不同组件的稳定性
- kotlin1.3的兼容性指南
- 常见问题
- FAQ
- 与Java比较
- 与Scala比较(官方已删除)
- Google开发者官网简介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin样品
- Kotlin零基础到进阶
- 第一阶段兴趣入门
- kotlin简介和学习方法
- 数据类型和类型系统
- 入门
- 分类
- val和var
- 二进制基础
- 基础
- 基本语法
- 包
- 示例
- 编码规范
- 代码注释
- 异常
- 根类型“Any”
- Any? 可空类型
- 可空性的实现原理
- kotlin.Unit类型
- kotlin.Nothing类型
- 基本数据类型
- 数值类型
- 布尔类型
- 字符型
- 位运算符
- 变量和常量
- 语法和运算符
- 关键字
- 硬关键字
- 软关键字
- 修饰符关键字
- 特殊标识符
- 操作符和特殊符号
- 算术运算符
- 赋值运算符
- 比较运算符
- 逻辑运算符
- this关键字
- super关键字
- 操作符重载
- 一元操作符
- 二元操作符
- 字符串
- 字符串介绍和属性
- 字符串常见方法操作
- 字符串模板
- 数组
- 数组介绍创建及遍历
- 数组常见方法和属性
- 数组变化以及下标越界问题
- 原生数组类型
- 区间
- 正向区间
- 逆向区间
- 步长
- 类型检测与类型转换
- is、!is、as、as-运算符
- 空安全
- 可空类型变量
- 安全调用符
- 非空断言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函数
- 函数式编程概述
- OOP和FOP
- 函数式编程基本特性
- 组合与范畴
- 在Kotlin中使用函数式编程
- 函数入门
- 函数作用域
- 函数加强
- 命名参数
- 默认参数
- 可变参数
- 表达式函数体
- 顶层、嵌套、中缀函数
- 尾递归函数优化
- 函数重载
- 控制流
- if表达式
- when表达式
- for循环
- while循环
- 循环中的 Break 与 continue
- return返回
- 标签处返回
- 集合
- list集合
- list集合介绍和操作
- list常见方法和属性
- list集合变化和下标越界
- set集合
- set集合介绍和常见操作
- set集合常见方法和属性
- set集合变换和下标越界
- map集合
- map集合介绍和常见操作
- map集合常见方法和属性
- map集合变换
- 集合的函数式API
- map函数
- filter函数
- “ all ”“ any ”“ count ”和“ find ”:对集合应用判断式
- 别样的求和方式:sumBy、sum、fold、reduce
- 根据人的性别进行分组:groupBy
- 扁平化——处理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 区间、数组、集合之间转换
- 面向对象
- 面向对象-封装
- 类的创建及属性方法访问
- 类属性和字段
- 构造器
- 嵌套类(内部类)
- 枚举类
- 枚举类遍历&枚举常量常用属性
- 数据类
- 密封类
- 印章类(密封类)
- 面向对象-继承
- 类的继承
- 面向对象-多态
- 抽象类
- 接口
- 接口和抽象类的区别
- 面向对象-深入
- 扩展
- 扩展:为别的类添加方法、属性
- Android中的扩展应用
- 优化Snackbar
- 用扩展函数封装Utils
- 解决烦人的findViewById
- 扩展不是万能的
- 调度方式对扩展函数的影响
- 被滥用的扩展函数
- 委托
- 委托类
- 委托属性
- Kotlin5大内置委托
- Kotlin-Object关键字
- 单例模式
- 匿名类对象
- 伴生对象
- 作用域函数
- let函数
- run函数
- with函数
- apply函数
- also函数
- 标准库函数
- takeIf 与 takeUnless
- 第二阶段重点深入
- Lambda编程
- Lambda成员引用高阶函数
- 高阶函数
- 内联函数
- 泛型
- 泛型的分类
- 泛型约束
- 子类和子类型
- 协变与逆变
- 泛型擦除与实化类型
- 泛型类型参数
- 泛型的背后:类型擦除
- Java为什么无法声明一个泛型数组
- 向后兼容的罪
- 类型擦除的矛盾
- 使用内联函数获取泛型
- 打破泛型不变
- 一个支持协变的List
- 一个支持逆变的Comparator
- 协变和逆变
- 第三阶段难点突破
- 注解和反射
- 声明并应用注解
- DSL
- 协程
- 协程简介
- 协程的基本操作
- 协程取消
- 管道
- 慕课霍丙乾协程笔记
- Kotlin与Java互操作
- 在Kotlin中调用Java
- 在Java中调用Kotlin
- Kotlin与Java中的操作对比
- 第四阶段专题练习
- 朱凯Kotlin知识点总结
- Kotlin 基础
- Kotlin 的变量、函数和类型
- Kotlin 里那些「不是那么写的」
- Kotlin 里那些「更方便的」
- Kotlin 进阶
- Kotlin 的泛型
- Kotlin 的高阶函数、匿名函数和 Lambda 表达式
- Kotlin协程
- 初识
- 进阶
- 深入
- Kotlin 扩展
- 会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions / Properties)
- Kotlin实战-开发Android