Kotlin协程学习(二):挂起函数

本文是对 Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了 的文字记录,确实香啊!

1. Kotlin 协程挂起的基本了解

Q:Kotlin 协程中挂起的是什么?

A:挂起的是协程。

Q:那什么是协程?

A:kotlin 协程为 launch()、async() 等中的代码。

launch 在创建的一个协程,在执行到某一个 suspend 函数(挂起函数)时,这个协程会被 suspend(被挂起)。

Q:从哪挂起?

A:从当前线程挂起,就是这个协程从正在执行它的线程上脱离了,注意不是协程停下来了,而是协程所在的线程从这行代码开始不再运行这个协程了(意思就是挂起函数将由自己指定的线程运行,挂起可以理解为将协程挂到指定的线程中去执行),那么分离了的线程和协程会各自发生什么?

1
2
3
4
5
6
launch(Dispatchers.Main){
...
//suspendingGetImage 为挂起函数
val image = suspendingGetImage(imageId)
imageIv.setImageBitmap(image)
}

分离了的线程和协程将如何执行?线程和协程分离了,具体到代码是什么意思?

2.分离后的线程

协程的代码块,在线程中到了 suspend 函数的时候,突然执行完毕了,返回了,完毕之后线程该干嘛呢?当然该干嘛干嘛去。

  • 线程为后台线程
    如果线程是后台线程,那么接下来该线程就没有事或者去执行其他的后台任务,和 Java 中的线程池中线程做完工作是一样的,要么回收掉,要么再利用;
  • 线程为主线程
    如果是 Android 中的主线程,那么它会继续执行接下来的工作。
    以上代码相当于:
1
2
3
4
5
handler.post{
...
val image = suspendingGetImage(imageId)
imageIv.setImageBitmap(image)
}

此时挂起就相当于 post 的这个任务提前结束了。

3. 分离后的协程

函数的代码在执行到挂起函数的时候被掐断了,所以接下来,它会在指定线程中从这个挂起函数开始向下执行,那么这个线程是谁指定的?当然是挂起函数指定的。在上面的例子钟就是 suspendingGetImage 这个函数中通过 withContext 指定的。

1
2
3
4
5
suspend fun suspendingGetImage(imageId:String){
withContext(Dispatchers.IO){
getImage(imageId)
}
}

那么在此例中,所指定的线程为函数内部的 withContext() 所指定的 IO 线程。

在挂起函数执行完毕后,它 会自动把线程再切回来

在上面的例子中,在挂起函数执行完毕后,协程会再 post 一个任务,让剩下的代码继续回到主线程中去执行

这也就是为什么协程中指定线程的参数不是 Thread ,而是 Dispatchers(调度器),调度器不止能指定协程执行的线程,还能在 suspend 挂起函数执行完毕后再自动切回来,当然也可以指定特殊的 Dispatchers ,在指定挂起函数后不切换回来。

挂起的含义就是:暂时切走,稍后在切回来就是切换线程,不过在执行完毕会切换回来,这个切回来的动作在协程中称为 resume (恢复)*

4. 为什么挂起函数只能在协程里或者另一个挂起函数中调用?

首先,挂起之后是需要恢复的,也就是把线程给切回来,而恢复这个动作是协程的,所以一个挂起函数不在协程/另一个挂起函数中被调用,那么这个恢复的动作是不能实现的。

5. 挂起是怎么做到的

在 挂起函数中使用 withContext 指定切换的线程。

其实 suspend 关键字并不能起到挂起函数的作用,并不是因为使用 suspend 修饰,函数就可以实现挂起动作,而真正想要挂起协程,还需要在挂起函数里面去调用另外一个挂起函数,而里面的这个挂起函数需要是协程自带的、内部实现了协程挂起代码的,或者它的内部直接或间接的调用了某一个自带的挂起函数,最终需要调用一个协程自带的挂起函数,让它来做真正的挂起,实现线程切换的工作。

6. suspend 真正的作用:提醒

既然 suspend 不能真正的做到挂起函数,那么 suspend 的作用是什么呢?

在语法上 suspend 的作用:它其实是一个 提醒,函数的创建者对函数的调用者的提醒:我是一个耗时函数,因此我被我的创建者用挂起函数的方式放到了后台运行,所以请在协程中调用我。

提醒调用者该函数为耗时操作,那么这个提醒有什么作用呢?

这个提醒有效避免主线程的卡顿,因为一旦不小心在主线程调用了一个耗时操作,那么主线程会产生卡顿。而协程通过挂起函数这种形式,把耗时任务切换线程的工作交给了函数的创建者而不是调用者,对于调用者会十分的简单,他看到 suspend 这个关键字就明白了应该在协程中调用,从而避免了调用者不熟悉调用的函数的情况下在主线程调用耗时操作的行为。

所以创建一个挂起函数,一定要在内部调用别的挂起函数,否则这个挂起函数就是没有必要的。

1
2
3
4
// 这个挂起函数是没有意义的
suspend fun test(str:String){
sout(str)
}

7. 自定义挂起函数

  • 什么时候需要自定义挂起函数

    • 原则:耗时(特殊:等待)

如果某个函数比较耗时,两类:

  • I/O 操作
  • 计算工作

比如文件读写、网络交互、图片操作等,还有在等待情况下,比如等待 5s 后继续操作,这些都可以写在挂起函数中。

  • 怎么写

函数添加关键字 suspend,内部代码使用 withContext获取他挂起函数包裹。


知识链接

Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了