Kotlin 协程与挂起函数及suspend关键字深入理解
作者:无糖可乐爱好者
1.挂起函数
挂起函数在Kotlin协程中是一个比较重要的知识点,协程的非阻塞式、Channel、Flow等API都对它有充分的理解才能在学习时事半功倍。
已知的是Kotlin协程的特点是轻量和非阻塞, 单靠这两个点就能说明Kotlin协程就的优势吗,不一定。这里先提出一个结论:挂起函数是Kotlin协程的最大优势。 下面对于挂起函数的讲解就围绕这句话展开。
以获取省市区Code为例,获取区域Code的前提是要有城市Code,城市Code的前提是要有省份Code,请求结果通过CallBack返回。
public class RequestCode { public static void main(String[] args) { getProvincesCode(provincesCode -> { getCityCode(provincesCode, cityCode -> { getAreaCode(cityCode, areaCode -> { }); }); }); } /** * 获取省份Code * * @param callBack */ private static void getProvincesCode(CallBack callBack) { callBack.onSuccess("100000"); } /** * 获取城市Code * * @param provincesCode * @param callBack */ private static void getCityCode(String provincesCode, CallBack callBack) { callBack.onSuccess("100010"); } /** * 获取区域code * * @param cityCode * @param callBack */ private static void getAreaCode(String cityCode, CallBack callBack) { callBack.onSuccess("100011"); } }
上面的代码在开发中都遇到过,代码可以优化的更简洁更易读,这里主要是证明挂起函数的优势,就不做优化了。
上面的代码可以看到三层嵌套是比较复杂的,如果再加上获取国家、区域街道的话嵌套层级会更深,这样就会对可读性、可扩展性、可维护性都有影响。那么用Kotlin这个代码要怎么写呢?
现在我用delay(1000L)
替代CallBack
模拟网络请求
fun main() = runBlocking { val provincesCode = getProvincesCode() val cityCode = getCityCode(provincesCode) val areaCode = getAreaCode(cityCode) } /** * 获取省份Code * */ private suspend fun getProvincesCode(): String { withContext(Dispatchers.IO) { delay(1000L) } return "100000" } /** * 获取城市Code * * @param provincesCode */ private suspend fun getCityCode(provincesCode: String): String { withContext(Dispatchers.IO) { delay(1000L) } return "100010" } /** * 获取区域code * * @param cityCode */ private suspend fun getAreaCode(cityCode: String): String { withContext(Dispatchers.IO) { delay(1000L) } return "100011" }
Kotlin实现了同步方式完成异步请求,这种方式的实现要归功于具体的函数的实现,在这三个函数中可以发现他们的定义与普通函数的区别是多了一个suspend
关键字,它的意思就是挂起。
再回头看main
函数,前面加上了runBlocking
也就是说建立了一个协程的作用域,这是因为suspend
的出现,因为suspend
的作用就是挂起和恢复,而挂起和恢复是需要上下文的,因此就需要定义一个作用域,这里选择了runBlocking
。
在函数中还有一个地方withContext(Dispatchers.IO)
这主要是执行线程的切换,除了IO
线程以外还有Main
、default
等线程。
在Kotlin中挂起和恢复是成对出现的,因为既然会被挂起那肯定也会被恢复,而协程的非阻塞也是因为挂起和恢复能力,那么挂起和恢复又是怎么样的一个含义呢?
首先执行getProvincesCode()
这是一个挂起函数,因为用了Dispatchers.IO
切换到了IO线程因此这个等待时间就在IO线程执行,当等待时间结束后(CallBack回调结果)provincesCode
收到返回的结果,这个函数中suspend
挂起,main
函数中收到结果就是恢复能力,
我们看一下debug日志:
- 这是
getProvincesCode
的协程日志,可以获取以下几个信息:
- 当前协程的名字是:coroutine#1
- 协程运行在主线程:main
- 任务被挂起
withContext(Dispatchers.IO)
切换线程
任务切换到DefaultDispatcher-worker-1
线程继续执行
return "省:100000"
回到主线程执行
我们总结一下上面的日志:
main
→DefaultDispatcher-worker-1
的过程是挂起;DefaultDispatcher-worker-1
→main
的过程是恢复;
那么挂起和恢复的含义也有非常明确了:
- 挂起: 只是将程序执行流程转移到了其他线程,主线程并不会阻塞。我们知道Android中如果阻塞超过一定时间就会出现无响应,上面的代码在运行过程中并不会导致无响应的发生,在代码执行的过程中我们还可以做其他事情的,因为任务进入到子线程后主线程的状态是空闲的。
- 恢复: 当子线程的任务执行完毕后再回到主线程的过程就叫做恢复。
挂起和恢复的能力是挂起函数特有的能力,普通函数时不具备的,如果在一个普通函数中仅仅加上suspend
关键字会发现其实并没有什么用,编译器会告知这种定义是多余的。
上面还提出了一个结论——挂起函数是Kotlin协程的最大优势, 首先因为函数的执行过程中可以切换线程,其次函数的运行不会影响主线程导致主线程被阻塞。
2.深入理解suspend
上面挂起函数,挂起函数主要就是主线程切换到子线程,这个过程又是如何实现的?
已知挂起函数是依靠关键字suspend
,我们将带有这个关键子的函数转换成Java代码进行分析:
private static final Object getProvincesCode(Continuation var0) { ... CoroutineContext var10000 = (CoroutineContext)Dispatchers.getIO(); ... return "省:100000"; }
代码比较长,只保留了需要的地方。
getProvincesCode
里面多了一个参数Continuation
,意思是延续,而suspend
没有了,那么Continuation
又是什么
public interface Continuation<in T> { /** * 协程的上下文. */ public val context: CoroutineContext /** * 恢复执行相应的协程,传递一个成功或失败的[result]作为最后一个挂起点的返回值 */ public fun resumeWith(result: Result<T>) }
从Continuation
的源码发现一个问题,它跟我们开头的CallBack
是类似的,其中resumeWith
和onSucess
的功能也是一样的,区别在于Continuation
是有一个泛型的参数
public interface CallBack { public void onSuccess(String response); } public interface Continuation<in T> { public fun resumeWith(result: Result<T>) }
由此可以得出结论:Continuation
本质上就是CallBack
只是多了一个带有泛型的参数。
通过上面的分析可以得出结论 :挂起函数的本质就是Callback
我们已知Continuation
的本质是CallBack
,Continuation
是延续,就是接下来要做的事情,那么在省市区获取的代码其实就是这样一个流程:
getProvincesCode(object : Continuation<String> { override fun resumeWith(result: Result<String>) { val provinceCode = result.getOrNull() println("provincesCode:$provinceCode") getCityCode(provinceCode, object : Continuation<String> { override fun resumeWith(result: Result<String>) { val cityCode = result.getOrNull() println("cityCode:$cityCode") getAreaCode(cityCode, object : Continuation<String> { override fun resumeWith(result: Result<String>) { val areaCode = result.getOrNull() println("areaCode:$areaCode") } }) } }) } })
这个过程是编译器在后面帮我们做的,实际编码中这种编码方式并不会出现,毕竟挂起函数解决的就是这个问题。
3.协程与挂起函数
需要说明的是协程与挂起函数并不是同一个东西,我们再最开始使用suspend
实现省市区三级联动的时候用到了runBlocking
,挂起函数的调用是需要一个协程作用域的,除了这点之外,在runBlocking
的源码中还有一点要注意:
public actual fun <T> runBlocking( context: CoroutineContext, block: suspend CoroutineScope.() -> T): T { ... }
第二个参数中有一个suspend
,所以可以理解CoroutineScope.() -> T
也是一个挂起函数那么被suspend
关键字修饰的挂起函数可以运行在runBlocking
中也就不难理解了。
那么我们就可以得到一个结论:挂起和恢复是协程的底层能力,而挂起函数是一种表现形式,通过suspend关键字修饰的函数可以让我们在上层很方便的实现这种底层能力。
4.挂起函数是Kotlin协程的最大优势
开篇就提出了这个结论,最后再来总结一下这个能力:
- 挂起函数在执行中可以切换线程且不会对主线程造成阻塞;
- 挂起函数有挂起和恢复的能力,可以极大地简化异步编程,实现同步执行异步任务;
- 挂起函数是Kotlin中特有的能力;
5.总结
- 定义一个挂起函数只需要在普通函数上加上
suspend
关键字,而添加这个关键字之后函数类型就会被改变,如suspend (Int) -> Double”与“(Int) -> Double
并不是同一个类型; - 挂起函数具有挂起和恢复的能力,那么就会出现同一行代码会在两个线程中执行,Kotlin编译器会在后台进行编译;
- 挂起函数的本质就是CallBack。只是说,Kotlin 底层用了一个更加高大上的名字,叫 Continuation;
- 挂起和恢复是一种底层能力,而上层的表现形式是挂起函数;
- 挂起函数只能在协程中被调用或者在挂起函数中调用,而协程中的block也是一个挂起函数。
以上就是Kotlin 协程与挂起函数及suspend关键字深入理解的详细内容,更多关于Kotlin 协程挂起函数suspend的资料请关注脚本之家其它相关文章!