在学习协程时,我们看到了suspend
关键字,那么为什么它能实现这么神奇的功能?我们知道在JVM平台,kotlin代码最终也会编译成字节码,那么编译器到底对suspend
关键字进行了哪些处理?明白了这其中的奥秘能够会让我们更好理解为什么suspend
不会立即返回结果而且还不会阻塞线程。
协程简化了在Android系统上的异步操作,我们知道在Android系统上主线程是不能阻塞,否则会造成卡顿甚至ANR
,所以如果有耗时任务,需要使用其它线程来运行耗时任务,然后通过回调的形式将任务结果返回到主线程处理,但是如果是多个耗时任务需要顺序执行才能拿到最终结果,这样就会使得代码非常不利于理解,比如:
fun loginUser(userId: String, password: String, userResult: Callback<User>) {
userRemoteDataSource.logUserIn { user ->
userLocalDataSource.logUserIn(user) { userDb ->
userResult.success(userDb)
}
}
}
而协程的出现解决了这一问题,使得代码变得更简洁、可读性更高。使用协程完成上诉代码会是这样的:
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
我们使用了suspend
关键字,这表明这个方法需要在协程中执行,我们可以简单理解为,这个方法内部可能在某些时刻挂起(暂停运行),在某些时刻恢复运行。与回调的方式相比,使用协程让线程切换和异常处理变得非常简单,那么这到底是如何实现的呢?
实际上,Kotlin编译器编译suspend
方法之后会通过有限状态机将转换为优化版的回调,也就是说它最终也是使用回调的形式实现的,但是这些回调不是我们自己写的,而是编译器帮我们完成的,那么具体是怎么转换的呢?
首先,在转换之后,suspend方法的返回值会被改为Unit,另外参数中会多出一个Continuation<Any?>类型的参数,比如上面的loginUser
方法会转化为:
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
completion.resume(userDb)
}
Continuation是suspend
方法之间的桥梁,它保存的一些当前方法调用的信息和状态,它的定义如下:
interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(value: Result<T>)
}
当一个suspend
方法被编译时,它会根据当前方法去生成一个实现了Continuation的类,这个类中就保存了当前方法运行的Dispatcher是什么,运行目前运行到方法内部的哪个suspend
方法了以及他们的返回值是什么,是否有异常等等信息。
以上面的loginUser
方法为例,当编译器编译该方法时,首先会识别到当前方法是suspend
方法,然后会找到在该方法中调用的其它suspend
方法,然后会把每个方法调用点作为有限状态机中的一个状态,后面会根据状态机的状态来分别调用,大概是这样的:
1fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
2 when(completion.label) {
3 0 -> { // Label 0 -> first execution
4 userRemoteDataSource.logUserIn(userId, password)
5 }
6 1 -> { // Label 1 -> resumes from userRemoteDataSource
7 userLocalDataSource.logUserIn(user)
8 }
9 2 -> { // Label 2 -> resumes from userLocalDataSource
10 completion.resume(userDb)
11 }
12 else -> throw IllegalStateException(...)
13 }
14}
那么又是如何知道当前该执行哪个代码块呢?或者确定当前的label值是什么?
前面提到了,编译器会为每个suspend
方法生成一个实现了Continuation接口的类,实际上是继承于ContinuationImpl的一个子类,这个类中保存了当前的label值和每个suspend
方法的返回值
1fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
2 class LoginUserStateMachine(
3 completion: Continuation<Any?>
4 ): ContinuationImpl(completion) {
5 // 当前suspend方法中一些局部变量
6 var user: User? = null
7 var userDb: UserDb? = null
8 // 通用的变量
9 var result: Any? = null
10 var label: Int = 0
11 // 当内部的suspend方法完成之后回调resume方法时,会触发这里
12 override fun invokeSuspend(result: Any?) {
13 this.result = result
14 loginUser(null, null, this)
15 }
16 }
17 ...
18}
当前loginUser
方法根据状态分为了几部分代码,每一部分触发的时机不一样,当内部的suspend方法执行完成返回结果时,会调用completion.resume(...)
方法,这个时候就会触发invokeSuspend
方法,这时状态已经变了,当调用loginUser(null, null, this)
时就会执行另外一部分代码,完整的参考代码如下:
1fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
2
3 class LoginUserStateMachine(
4 completion: Continuation<Any?>
5 ): ContinuationImpl(completion) {
6 // 当前suspend方法中一些局部变量
7 var user: User? = null
8 var userDb: UserDb? = null
9 // 通用的变量
10 var result: Any? = null
11 var label: Int = 0
12 // 当内部的suspend方法完成之后回调resume方法时,会触发这里
13 override fun invokeSuspend(result: Any?) {
14 this.result = result
15 loginUser(null, null, this)
16 }
17 }
18
19 val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
20
21 when(continuation.label) {
22 0 -> {
23 //检查是否有异常,有异常则抛出
24 throwOnFailure(continuation.result)
25 // 更新状态,下次当前loginUser方法时,就会走state=1的代码
26 continuation.label = 1
27 // 把当前的continuation传入到下一个suspend方法,当它完成时调用resume方法就会回调当前的loginUser方法
28 userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
29 }
30 1 -> {
31 //检查是否有异常,有异常则抛出
32 throwOnFailure(continuation.result)
33 // 拿到结果上一个suspend方法返回的结果
34 continuation.user = continuation.result as User
35 // 更新状态,下次当前loginUser方法时,就会走state=2的代码
36 continuation.label = 2
37 // 把当前的continuation传入到下一个suspend方法,当它完成时调用resume方法就会回调当前的loginUser方法
38 userLocalDataSource.logUserIn(continuation.user, continuation)
39 }
40 2 -> {
41 //检查是否有异常,有异常则抛出
42 throwOnFailure(continuation.result)
43 // 拿到结果上一个suspend方法返回的结果
44 continuation.userDb = continuation.result as UserDb
45 // 把当前结果返回到上一级调用
46 continuation.completion.resume(continuation.userDb)
47 }
48 else -> throw IllegalStateException(...)
49 }
50}
上面只提到了suspend方法被编译后转换之后大概是什么样的,但是并没有提到线程切换,其实线程切换就很简单了,因为Continuation是保存了当前协程的context,当我们执行完当前方法时,我们会调用从上一个方法传过来的Continuation,也就是completion成员变量的resume方法,而resume方法就可以根据context来切换协程执行环境了。