在学习协程时,我们看到了suspend
关键字,那么为什么它能实现这么神奇的功能?我们知道在JVM平台,kotlin代码最终也会编译成字节码,那么编译器到底对suspend
关键字进行了哪些处理?明白了这其中的奥秘能够会让我们更好理解为什么suspend
不会立即返回结果而且还不会阻塞线程。
协程简化了在Android系统上的异步操作,我们知道在Android系统上主线程是不能阻塞,否则会造成卡顿甚至ANR
,所以如果有耗时任务,需要使用其它线程来运行耗时任务,然后通过回调的形式将任务结果返回到主线程处理,但是如果是多个耗时任务需要顺序执行才能拿到最终结果,这样就会使得代码非常不利于理解,比如:
而协程的出现解决了这一问题,使得代码变得更简洁、可读性更高。使用协程完成上诉代码会是这样的:
我们使用了suspend
关键字,这表明这个方法需要在协程中执行,我们可以简单理解为,这个方法内部可能在某些时刻挂起(暂停运行),在某些时刻恢复运行。与回调的方式相比,使用协程让线程切换和异常处理变得非常简单,那么这到底是如何实现的呢?
实际上,Kotlin编译器编译suspend
方法之后会通过有限状态机将转换为优化版的回调,也就是说它最终也是使用回调的形式实现的,但是这些回调不是我们自己写的,而是编译器帮我们完成的,那么具体是怎么转换的呢?
首先,在转换之后,suspend方法的返回值会被改为Unit,另外参数中会多出一个Continuation<Any?>类型的参数,比如上面的loginUser
方法会转化为:
Continuation是suspend
方法之间的桥梁,它保存的一些当前方法调用的信息和状态,它的定义如下:
当一个suspend
方法被编译时,它会根据当前方法去生成一个实现了Continuation的类,这个类中就保存了当前方法运行的Dispatcher是什么,运行目前运行到方法内部的哪个suspend
方法了以及他们的返回值是什么,是否有异常等等信息。
以上面的loginUser
方法为例,当编译器编译该方法时,首先会识别到当前方法是suspend
方法,然后会找到在该方法中调用的其它suspend
方法,然后会把每个方法调用点作为有限状态机中的一个状态,后面会根据状态机的状态来分别调用,大概是这样的:
那么又是如何知道当前该执行哪个代码块呢?或者确定当前的label值是什么?
前面提到了,编译器会为每个suspend
方法生成一个实现了Continuation接口的类,实际上是继承于ContinuationImpl的一个子类,这个类中保存了当前的label值和每个suspend
方法的返回值
当前loginUser
方法根据状态分为了几部分代码,每一部分触发的时机不一样,当内部的suspend方法执行完成返回结果时,会调用completion.resume(...)
方法,这个时候就会触发invokeSuspend
方法,这时状态已经变了,当调用loginUser(null, null, this)
时就会执行另外一部分代码,完整的参考代码如下:
上面只提到了suspend方法被编译后转换之后大概是什么样的,但是并没有提到线程切换,其实线程切换就很简单了,因为Continuation是保存了当前协程的context,当我们执行完当前方法时,我们会调用从上一个方法传过来的Continuation,也就是completion成员变量的resume方法,而resume方法就可以根据context来切换协程执行环境了。