引入

  • 项目根目录下的 build.gradle : ```groovy buildscript { ext.kotlin_coroutines = ‘1.3.1’ }
  1. - Module 下的 `build.gradle` :
  2. ```groovy
  3. dependencies {
  4. // 依赖协程核心库
  5. implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
  6. // 依赖当前平台所对应的平台库
  7. implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
  8. }

基本使用

  1. // 方法一,使用 runBlocking 顶层函数
  2. runBlocking {
  3. }
  4. // 方法二,使用 GlobalScope 单例对象
  5. // 👇 可以直接调用 launch 开启协程
  6. GlobalScope.launch {
  7. }
  8. // 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
  9. // CoroutineContext(Dispatchers.Main、Dispatchers。IO等)
  10. // 👇 需要一个类型为 CoroutineContext 的参数
  11. val coroutineScope = CoroutineScope(context)
  12. coroutineScope.launch {
  13. }
  • 方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。
  • 方法二和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会和 app 一致,且不能取消(什么是协程的取消后面的文章会讲)。
  • 方法三是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)

    例子

    1. 网络请求

    1. CoroutineScope(Dispatchers.Main).launch { // 在主线程运行
    2. val data = withContext(Dispatchers.IO) { api.getGirlList(1, 10) } // 在IO线程执行网络请求
    3. //val data = async { api.getGirlList(1, 10) } // 或者可以这样写
    4. LogUtils.e(data.toString()) // 接口返回的数据
    5. }

    2. 请求合并;解决 请求嵌套

    假定:先拿到token才能拿到用户信息;用Java写只能强行请求嵌套
    1. coroutineScope.launch(Dispatchers.Main) { // 开始协程:主线程
    2. val token = api.getToken() // 网络请求:IO 线程
    3. val user = api.getUser(token) // 网络请求:IO 线程
    4. nameTv.text = user.name // 更新 UI:主线程
    5. }

    suspend:协程的挂起

「挂起」的本质

协程中「挂起」的对象到底是什么?挂起线程,还是挂起函数?都不对,我们挂起的对象是协程。
还记得协程是什么吗?启动一个协程可以使用 launch 或者 async 函数,协程其实就是这两个函数中闭包的代码块。
launchasync 或者其他函数创建的协程,在执行到某一个 suspend 函数的时候,这个协程会被「suspend」,也就是被挂起。
那此时又是从哪里挂起?从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。
注意,不是这个协程停下来了!是脱离,当前线程不再管这个协程要去做什么了。
suspend 是有暂停的意思,但我们在协程中应该理解为:当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了。
我们先让时间静止,然后兵分两路,分别看看这两个互相脱离的线程和协程接下来将会发生什么事情:
线程:
前面我们提到,挂起会让协程从正在执行它的线程上脱离,具体到代码其实是:
协程的代码块中,线程执行到了 suspend 函数这里的时候,就暂时不再执行剩余的协程代码,跳出协程的代码块。
那线程接下来会做什么呢?
如果它是一个后台线程:

  • 要么无事可做,被系统回收
  • 要么继续执行别的后台任务

跟 Java 线程池里的线程在工作结束之后是完全一样的:回收或者再利用。
如果这个线程它是 Android 的主线程,那它接下来就会继续回去工作:也就是一秒钟 60 次的界面刷新任务。
一个常见的场景是,获取一个图片,然后显示出来:

  1. // 主线程中
  2. GlobalScope.launch(Dispatchers.Main) {
  3. val image = suspendingGetImage(imageId) // 获取图片
  4. avatarIv.setImageBitmap(image) // 显示出来
  5. }
  6. suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
  7. ...
  8. }

可以看到 avatar 和 logo 的类型可以声明为 Deferred ,通过 await 获取结果并且更新到 UI 上显示。
await 函数签名如下:

  1. public suspend fun await(): T

怎么就「挂起」了?

我们了解到了什么是「挂起」后,再接着看看这个「挂起」是怎么做到的。
先随便写一个自定义的 suspend 函数:

  1. suspend fun suspendingPrint() {
  2. println("Thread: ${Thread.currentThread().name}")
  3. }
  4. I/System.out: Thread: main

输出的结果还是在主线程。
为什么没切换线程?因为它不知道往哪切,需要我们告诉它。
对比之前例子中 suspendingGetImage 函数代码:

  1. // 👇
  2. suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
  3. ...
  4. }

我们可以发现不同之处其实在于 withContext 函数。
其实通过 withContext 源码可以知道,它本身就是一个挂起函数,它接收一个 Dispatcher 参数,依赖这个 Dispatcher参数的指示,你的协程被挂起,然后切到别的线程。
所以这个suspend,其实并不是起到把任何把协程挂起,或者说切换线程的作用。
真正挂起协程这件事,是 Kotlin 的协程框架帮我们做的。
所以我们想要自己写一个挂起函数,仅仅只加上suspend 关键字是不行的,还需要函数内部直接或间接地调用到 Kotlin 协程框架自带的 suspend 函数才行。

suspend 的意义?

这个 suspend 关键字,既然它并不是真正实现挂起,那它的作用是什么?
它其实是一个提醒。
函数的创建者对函数的使用者的提醒:我是一个耗时函数,我被我的创建者用挂起的方式放在后台运行,所以请在协程里调用我。
为什么 suspend 关键字并没有实际去操作挂起,但 Kotlin 却把它提供出来?
因为它本来就不是用来操作挂起的。
挂起的操作 —— 也就是切线程,依赖的是挂起函数里面的实际代码,而不是这个关键字。
所以这个关键字,只是一个提醒
还记得刚才我们尝试自定义挂起函数的方法吗?

  1. // 👇 redundant suspend modifier
  2. suspend fun suspendingPrint() {
  3. println("Thread: ${Thread.currentThread().name}")
  4. }

如果你创建一个 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,它的作用是等待一段时间后再继续往下执行代码。
使用它就可以实现刚才提到的等待类型的耗时操作:

  1. suspend fun suspendUntilDone() {
  2. while (!done) {
  3. delay(5)
  4. }
  5. }