深入探索suspend关键字

深入学习Kotlin基础知识
2021-05-17 14:29 · 阅读时长11分钟

在学习协程时,我们看到了suspend关键字,那么为什么它能实现这么神奇的功能?我们知道在JVM平台,kotlin代码最终也会编译成字节码,那么编译器到底对suspend关键字进行了哪些处理?明白了这其中的奥秘能够会让我们更好理解为什么suspend不会立即返回结果而且还不会阻塞线程。

协程简化了在Android系统上的异步操作,我们知道在Android系统上主线程是不能阻塞,否则会造成卡顿甚至ANR,所以如果有耗时任务,需要使用其它线程来运行耗时任务,然后通过回调的形式将任务结果返回到主线程处理,但是如果是多个耗时任务需要顺序执行才能拿到最终结果,这样就会使得代码非常不利于理解,比如:

加载中...

而协程的出现解决了这一问题,使得代码变得更简洁、可读性更高。使用协程完成上诉代码会是这样的:

加载中...

我们使用了suspend关键字,这表明这个方法需要在协程中执行,我们可以简单理解为,这个方法内部可能在某些时刻挂起(暂停运行),在某些时刻恢复运行。与回调的方式相比,使用协程让线程切换和异常处理变得非常简单,那么这到底是如何实现的呢?

如何转换suspend方法?

实际上,Kotlin编译器编译suspend方法之后会通过有限状态机将转换为优化版的回调,也就是说它最终也是使用回调的形式实现的,但是这些回调不是我们自己写的,而是编译器帮我们完成的,那么具体是怎么转换的呢?

首先,在转换之后,suspend方法的返回值会被改为Unit,另外参数中会多出一个Continuation<Any?>类型的参数,比如上面的loginUser方法会转化为:

加载中...

Continuationsuspend方法之间的桥梁,它保存的一些当前方法调用的信息和状态,它的定义如下:

加载中...
  • context变量是与这个continuation关联的协程上下文。
  • resumeWith方法是用来恢复协程的,参数表示要返回的数据或者异常。

当一个suspend方法被编译时,它会根据当前方法去生成一个实现了Continuation的类,这个类中就保存了当前方法运行的Dispatcher是什么,运行目前运行到方法内部的哪个suspend方法了以及他们的返回值是什么,是否有异常等等信息。

以上面的loginUser方法为例,当编译器编译该方法时,首先会识别到当前方法是suspend方法,然后会找到在该方法中调用的其它suspend方法,然后会把每个方法调用点作为有限状态机中的一个状态,后面会根据状态机的状态来分别调用,大概是这样的:

加载中...

那么又是如何知道当前该执行哪个代码块呢?或者确定当前的label值是什么?

生成的状态机是什么样的?

前面提到了,编译器会为每个suspend方法生成一个实现了Continuation接口的类,实际上是继承于ContinuationImpl的一个子类,这个类中保存了当前的label值和每个suspend方法的返回值

加载中...

当前loginUser方法根据状态分为了几部分代码,每一部分触发的时机不一样,当内部的suspend方法执行完成返回结果时,会调用completion.resume(...)方法,这个时候就会触发invokeSuspend方法,这时状态已经变了,当调用loginUser(null, null, this)时就会执行另外一部分代码,完整的参考代码如下:

加载中...

参考:ContinuationImpl

注释
如何做到线程切换?

上面只提到了suspend方法被编译后转换之后大概是什么样的,但是并没有提到线程切换,其实线程切换就很简单了,因为Continuation是保存了当前协程的context,当我们执行完当前方法时,我们会调用从上一个方法传过来的Continuation,也就是completion成员变量的resume方法,而resume方法就可以根据context来切换协程执行环境了。

suspendkotlincoroutine协程android协程原理