这是《Kotlin 协程基础课》的第2篇文章。
Everyone must choose one of two pains: The pain of discipline or the pain of regret. Choose WISELY.
在上篇文章里我们学习了如何通过协程简化耗时操作的写法,其中有一个关键字suspend
,用于在定义函数时进行声明。本篇文章将对suspend进行进一步介绍,旨在学会它的含义和用法。
suspend限定词的含义
suspend,翻译过来就是中断,挂起,跟public、static等关键字相同,用在函数声明前,表示这是一个“挂起函数”。
挂起函数只能在协程或另一个挂起函数中被调用,如果你在非协程中使用到了挂起函数,会看到编译器有如下报错:
1 | Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function |
前面用到的delay
就是一个挂起函数。suspend关键字表明函数内部进行了耗时操作,可以是计算密集型的CPU任务,也可以是网络、磁盘操作密集型的IO任务。基本上可以看做“但凡用callback实现的回调函数,都能用一个相对应的挂起函数实现”。
所以我们使用suspend关键字的时机就非常明确了:当函数要进行耗时操作时,就把它声明为suspend
。
suspend做了什么事
方便起见,后续用“挂起”指代“suspend”。
作为及物动词,“挂起”应当有一个宾语,这里“挂起”的对象是协程。接下来我们对挂起的过程中发生了什么一探究竟,记住下面这句话:
“挂起”是指协程从它当前线程脱离,切换到另一个线程运行。当线程运行到suspend
函数时,会暂时挂起这个函数及后续代码的执行。这里涉及到两个角色:线程和协程。
线程的行为
当线程运行到“挂起”代码块时,会跳出当前的代码块,不再执行后续代码。接下来线程会做什么呢?
如果它是一个后台线程:
- 要么无事可做,被系统回收
- 要么被调度执行别的后台任务
跟Java线程池里的线程在工作结束之后的表现完全一样:回收或者再利用。
如果它是Android主线程:
- 继续UI刷新工作
协程的行为
上面讲到线程运行到挂起代码块时,会暂时退出当前代码块的执行。那么,剩余的协程代码在哪里得到执行呢?答案就在挂起函数的实现中——即我们为挂起函数指定的线程。
withContext
函数可以指定协程代码的运行线程,常见的Dispatcher有Main、IO、Default。协程从挂起的地方开始,切换到这些线程之中的一个继续运行,当运行完毕时,会自动切换回原线程执行。
在协程的源码里,“自动切换回来”是通过resume实现的。挂起函数之所以必须在协程中调用,就是因为协程框架会自动帮我们处理这个自动切换的过程。
suspend 不会真正操作挂起
并不是声明了suspend
后,线程运行到该位置,就自动进行挂起切换的,参照下面一个例子,挂起函数仍然运行在主线程中。为什么没有切换线程?因为编译器根本不知道要往哪里切,需要我们在编码时明确告诉它,这个挂起函数要切换到哪一个线程继续运行。我们可以用withContext
指明待切换的线程。在实现一个挂起函数时,仅仅加上suspend
关键字是不够的,必须在函数内部直接或间接地调用协程框架自带的suspend
函数。
suspend
只是一个提醒,它只有一个效果,就是限制函数只能在协程里调用,如果在非协程里使用了suspend
函数,则编译不通过。
1 | // 仍然运行在Main |
如何写一个suspend函数
最简单的方式是:
- 声明函数为
suspend
- 使用
withContext
指定目标线程,或者在函数内部调用另一个suspend
函数
小结
本文介绍了协程中最常见的关键字suspend
的含义和用法,阅读完本文后你应当掌握:
- 挂起函数运行时的表现
- 什么情况下使用挂起函数
- 如何写一个简单的挂起函数