kotlin协程的启动分析
讨论前提
以下讨论先不涉及协程状态管理。
启动目标
启动协程的实质上是将可被挂起的计算执行从头开始起来。suspend () -> T 或者 suspend R.() -> T 类型可以包含可挂起的计算,kotlin 协程库底层 api 启动协程时,实质上是去从头开始执行前面这两个类型中的可被挂起的计算。
为可被挂起执行的返回值。 为 receiver。
suspend () -> T / suspend R.() -> T 有如下 3 种情况
- 挂起 lambda 引用
- 挂起函数/方法引用
- Java 中实现的挂起方法引用
接下来只讨论 suspend R.() -> T 的情况,suspend () -> T 是类似的。
1. 挂起 lambda 引用
Kotlin
// suspend String.() -> String类型
val block: suspend String.() -> String = {
println("receiver $this")
"block 000"
}
// 通过反射获取BaseContinuationImpl类型的completion字段值
fun <T> Any.getCompletionField(): Continuation<Any?>? {
val baseContinuationClass = Class.forName("kotlin.coroutines.jvm.internal.BaseContinuationImpl")
val field: Field = baseContinuationClass.getDeclaredField("completion").apply { isAccessible = true }
return field.get(this) as Continuation<Any?>?
}
Java
// block指向的实际类型对应的反编译后的代码
Function2 block = new Function2((Continuation)null) {
int label;
// $FF: synthetic field
private Object L$0;
public final Object invokeSuspend(Object $result) {
IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (this.label) {
case 0:
ResultKt.throwOnFailure($result);
String var2 = (String)this.L$0;
System.out.println("receiver " + var2);
return "block 000";
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
public final Continuation create(Object value, Continuation $completion) {
Function2 var3 = new <anonymous constructor>($completion);
var3.L$0 = value;
return (Continuation)var3;
}
public final Object invoke(String p1, Continuation p2) {
return ((<undefinedtype>)this.create(p1, p2)).invokeSuspend(Unit.INSTANCE);
}
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object p1, Object p2) {
return this.invoke((String)p1, (Continuation)p2);
}
};
Kotlin
// 对block进行反射
// 构造器
block::class.declaredConstructors
// ..OOODemoKt$main$block$1(kotlin.coroutines.Continuation)
// 申明字段
block::class.declaredFields
// int ...OOODemoKt$main$block$1.label
// private java.lang.Object ...OOODemoKt$main$block$1.L$0"
// 父类
block::class.java.superclass
// class kotlin.coroutines.jvm.internal.SuspendLambda
block::class.java.superclass.superclass
// class kotlin.coroutines.jvm.internal.ContinuationImpl
block::class.superclass.superclass.superclass
// class kotlin.coroutines.jvm.internal.BaseContinuationImpl
// 反射获取父类中申明的completion字段
block.getCompletionField<Any?>()
// null
从反射结果推测如下情况:
- 编译器会给实际类型生成 1 个构造器:
Plaintext
实际类型(value:Continuation)
|
V
Suspendlambda(arity:Int,completion:Continuation<Any?>?) 或者 (arity: Int)
|
V
ContinuationImpl(completion: Continuation<Any?>?,_context: CoroutineContext?)
|
V
BaseContinuationImpl(completion: Continuation<Any?>?)
- 编译器会给实际类型生成 2 个字段。
completion字段只存在于BaseContinuationImpl的声明中,此时这个值为 null。由于不能确定创建实际类型时到底调用的Suspendlambda的哪个构造器,所以不能确定在创建实际类型时到底传递了什么给实际类型的构造器。
所以,当将一个 lambda 赋值给 suspend String.() -> String 时,实际上是调用实际类型 (value:Continuation) 构造器创建以后再进行赋值的。
Java
// val block: suspend String.() -> String = { }
Function2 block = new Function2((Continuation)null) {
//...
};
编译器还会实现 invokeSuspend() 方法,在这个方法中,编译器会按“不含挂起点代码 - 挂起点”格式将 lambda 中代码分成很多个代码片段,然后再结合一些模版代码来让 lambda 中的代码编程可被挂起的执行。所以只要调用到 invokeSuspend() 方法,可被挂起的执行就启动起来了。
Java
public final Object invokeSuspend(Object $result) {
IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (this.label) {
case 0:
ResultKt.throwOnFailure($result);
String var2 = (String)this.L$0;
System.out.println("receiver " + var2);
return "block 000";
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
编译器还会给这个子类型内部会生成一些字段,例如:
Java
int label;
private Object L$0;
这些字段会随着 invokeSuspend() 方法的执行(也可以叫做状态机的执行)而改变。所以如果你想每次从头开始执行 invokeSuspend() 中可挂起的计算,你需要先调用编译器重写 BaseContinuationImpl 中的 create(receiver, completion) 方法来创建一个新的状态机,然后再调用 invokeSuspend() 方法开始执行:
Java
public final Continuation create(Object value, Continuation $completion) {
Function2 var3 = new <anonymous constructor>($completion);
var3.L$0 = value;
return (Continuation)var3;
}
由于 “<anonymous constructor>” 的干扰,我们需要先确定 var3 的实际类型:
Kotlin
val completion = object : Continuation<Int> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Int>) {
println("completion is invoked, resumeWith result $result")
}
}
// 反射调用block的create(receiver,compeltion)方法创建的状态机
val continuationImpl: Continuation<Unit> = block.invokeCreateWithReceiver("woshireceiver", completion)
// completion字段
val completionFieldValue = continuationImpl.getCompletionField<Any?>()
// 确定var3 实际类型
println(block::class.java == continuationImpl::class.java)
// 确定create()的compeltion参数和BaseContinuationImpl.completion的关系
println(completionFieldValue === completion)
// 输出
// true
// true
上述测试代码表明:
当调用 block.create(receiver, completion) 时,在 create() 方法内部会调用 block 指向实际类的构造函数 (Continuation),并将 create 方法的第二个参数 completion 传递给它。这个 completion 参数最终会被赋值给在 block 指向实际类型父类中定义的 completion 字段。间接说明当传递给实际类型构造函数的值不为 null 时,Suspendlambda 的 (arity:Int, completion:Continuation<Any?>?) 构造器会被调用(当传递参数为 null 时,依然不能确定)。
Plaintext
myReceiver,myCompletion
|
V
create(myReceiver,myCompletion)
|
V
实际类型(myCompletion)
|
V
Suspendlambda(arity:Int,myCompletion)
|
V
ContinuationImpl(myCompletion,_context: CoroutineContext?)
|
V
BaseContinuationImpl(myCompletion)
当 invokeSuspend() 返回非 “COROUTINE_SUSPENDED” 的值,或者其中抛出未捕获异常时,表明它所在状态机对应的可被挂起计算执行完毕,接着就会调用 completion.resumeWith()。
Plaintext
可被挂起计算执行完毕
|
V
BaseContinuationImpl#completion.resumeWith()
而 completion 字段的值来源于 create(receiver, completion) 的第二个参数,所以 create(receiver, completion) 的第二个参数在它所在状态机对应可挂起计算执行完毕时被回调。
create(receiver, compeltion) 方法第一个参数 value 为例子中 lambda 的 receiver,在 lambda 中通过 this 关键字引用:
Plaintext
// 观察编译前代码println("receiver $this")可知对应的编译后代码为System.out.println("receiver " + var2);
// 所以var2就是this
// var2的值来源于this.L$0
// String var2 = (String)this.L$0;
// this.L$0的值来源于create()方法的第一个参数value
// var3.L$0 = value;
// 赋值流程:value -> L$0 -> var2(receiver)
// 所以,create()的第一个参数value最后会赋值给了lambda中的receiver
block 对应的实际类型还会实现 Function2 类型,可以通过反射验证:
Kotlin
public interface Function2<in P1, in P2, out R> : kotlin.Function<R> {
public abstract operator fun invoke(p1: P1, p2: P2): R
}
block::class.java.interfaces
// 说明实现了Function2
// interface kotlin.jvm.functions.Function2
block::class.java.methods
// 由于泛型擦除生成的桥接方法
// ..invoke(java.lang.Object,java.lang.Object)"
// 实现Function2的invoke()方法
// ..invoke(java.lang.String,kotlin.coroutines.Continuation)"
// ..省略其他方法
显然可以通过 (block as Function2<P1,P2,R>).invoke(p1,p2) 来调用它的 invoke 方法。接下来推测一下 P1, P2, R 分别可以使用什么类型。
根据 invoke() 方法的参数签名,显然泛型参数 P1 可以为 String,P2 可以至少为 Continuation。
Java
// suspend String.() -> String 指向的实际类型对应的反编译以后代码
// 中由编译器实现的invoke()方法
public final Object invoke(String p1, Continuation p2) {
return ((<undefinedtype>)this.create(p1, p2)).invokeSuspend(Unit.INSTANCE);
}
// $FF: synthetic method
// $FF: bridge method
// 由于泛型擦除特性,生成的桥接方法
public Object invoke(Object p1, Object p2) {
return this.invoke((String)p1, (Continuation)p2);
}
当调用 block.invoke(String p1, Continuation p2) 时:
Plaintext
block.invoke(p1,p2)
|
V
(<undefinedtype>)this.create(p1, p2)
|
V
(<undefinedtype>).invokeSuspend(Unit.INSTANCE)
所以:
p1为receiver,在 lambda 中通过this关键字引用。p2为completion,当 lambda 中可被挂起计算执行完毕以后可能会调用它的resumeWith()方法。invoke()内部先通过create()创建一个新的状态机,这个状态机类型和block的类型一致,只是它的receiver和completion被赋值了。然后调用invokeSuspend()方法开始执行可被挂起的计算。
那么,当拿到了 block:String.() -> String 类型的引用时,如何调用它的 invoke() 方法呢?根据 invoke() 方法返回值,可以得出 R 可以为 Any?。目前这个实际类型实现的 Function2 的样子如下:
Kotlin
Function2<String, Continuation, Any?>
现在还缺少 Continuation 的泛型参数,实在不知道如何推测,直接参考协程库中的代码:
Kotlin
// IntrinsicsJvm.kt
public actual inline fun <T> (suspend () -> T).startCoroutineUninterceptedOrReturn(
completion: Continuation<T>
): Any? =
if (this !is BaseContinuationImpl) wrapWithContinuationImpl(completion)
// 参考这里
else (this as Function1<Continuation<T>, Any?>).invoke(completion)
所以最终的样子为:
Kotlin
Function2<String, Continuation<String>, Any?>
// 强转以后,调用invoke()方法
(block as Function2<String, Continuation<String>, Any?>).invoke(receiver, completion)
在当前 kotlin 协程库的实现中,从非挂起函数环境下,从头开始执行 invokeSuspend() 中可挂起计算,一般有 2 种方式:
- 直接或者间接调用
BaseContinuationImpl.resumeWith(Unit) - 调用编译器生成的
invoke(receiver, completion)方法
方式 1:
Kotlin
// ContinuationImpl.kt
internal abstract class BaseContinuationImpl(
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result<Any?>) {
var current = this
var param = result
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!! // fail fast when trying to resume continuation without completion
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
current = completion
param = outcome
} else {
completion.resumeWith(outcome)
return
}
}
}
}
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
//..省略代码
public open fun create(completion: Continuation<*>): Continuation<Unit> {
throw UnsupportedOperationException("create(Continuation) has not been overridden")
}
public open fun create(value: Any?, completion: Continuation<*>): Continuation<Unit> {
//..省略代码
}
//..省略代码
}
观察 BaseContinuationImpl 源码可知,调用 BaseContinuationImpl#resumeWith() 会间接调用到 invokeSuspend()。所以如果状态机是刚刚通过 create(receiver, completion) 创建的,只要调用 resumeWith(Result.success(Unit)) 方法就可以从头开始执行 invokeSuspend() 中的可被挂起的计算。
Kotlin
// intercepted()需要手动导入,idea不会自动导入它
import kotlin.coroutines.intrinsics.intercepted
@OptIn(InternalCoroutinesApi::class)
fun main() {
val block: suspend String.() -> String = {
println("run block ,receiver $this")
"block 000"
}
val completion = object : Continuation<String> {
override val context: CoroutineContext
// 间接调用例子用到
get() = Dispatchers.IO
override fun resumeWith(result: Result<String>) {
println("completion is invoked, resumeWith result $result")
}
}
//1. 反射调用create(receiver,completion)方法
val receiver = "woshireceiver"
val continuationImpl: Continuation<Unit> = block.invokeCreateWithReceiver(receiver, completion)
//2.1 直接调用
// continuationImpl.resumeWith(Result.success(Unit))
//2.2 间接调用
continuationImpl
.intercepted()
.resumeWith(Result.success(Unit))
// 避免线程退出
while (true){}
}
// 反射调用BaseContinuationImpl.create(receiver,completion)方法
fun <R, T> Any.invokeCreateWithReceiver(receiver: R, completion: Continuation<T>): Continuation<Unit> {
try {
val baseContinuationClass = Class.forName("kotlin.coroutines.jvm.internal.BaseContinuationImpl")
val createMethod: Method = baseContinuationClass.getMethod(
"create",
Any::class.java,
Continuation::class.java
).apply {
isAccessible = true
}
return createMethod.invoke(this, receiver, completion) as Continuation<Unit>
} catch (e: Exception) {
throw e
}
}
先调用 create(receiver, completion) 方法,然后再直接或者间接的调用 resumeWith(Result.success(Unit))。这种方式可以让你来决定 invokeSuspend() 方法中可挂起计算从头开始执行的时机。例如你可以直接调用 continuationImpl.resumeWith(..) 让它立即开始执行,或者通过上述间接调用的例子让执行时机由 Dispatchers.IO 来决定。
方式 2:
这种方式是先调用 create() 方法创建一个状态机,紧接着立马调用 invokeSuspend() 方法开始从头执行其中的可挂起计算。这让这个 invoke() 方法具有了如下特性:
- 如果可挂起计算中没有挂起点挂起,并且没有未捕获异常,
invoke()方法直接返回结果,后续completion不会被回调。 - 如果可挂起计算中有挂起点挂起,并且没有未捕获异常,
invoke()方法直接返回COROUTINE_SUSPENDED,后续completion会被回调。 - 如果可挂起计算中没有挂起点挂起,但是有未捕获的异常,这个异常可以被包住
invoke()的 trycatch 块捕获,后续completion不会被回调。 - 如果可挂起计算中有挂起点挂起,但是在执行到第一个挂起点挂起之前有未捕获的异常,这个异常可以被包住
invoke()的 trycatch 块捕获,后续completion不会被回调。
Kotlin
//将block1/block2/block3/block4赋值给block来测试
val block = null
val completion = object : Continuation<Int> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<Int>) {
println("completion is invoked, resumeWith result $result")
}
}
val function2: Function2<String, Continuation<Int>, Any?> =
block as Function2<String, Continuation<Int>, Any?>
val receiver = "woshireceiver"
try {
val invokeResult = function2.invoke(receiver, completion)
println("invokeResult $invokeResult")
} catch (e: Exception) {
println("捕获invoke的异常:$e")
}
// 没有挂起点,没有未捕获异常
// block1
val block1: suspend String.() -> String = {
println("run block1 ,receiver $this")
"block 000"
}
// 有挂起点,没有未捕获异常
val block2: suspend String.() -> String = {
// 挂起点1
suspendCoroutineUninterceptedOrReturn<String> {
thread {
Thread.sleep(3000L)
println("第一个挂起点resumeWith")
it.resumeWith(Result.success("block 222"))
}
println("第一个挂起点suspended")
COROUTINE_SUSPENDED
}
// 挂起点2
suspendCoroutineUninterceptedOrReturn<String> {
thread {
Thread.sleep(1000L)
println("第二个挂起点resumeWith")
it.resumeWith(Result.success("block 222"))
}
println("第二个挂起点suspended")
COROUTINE_SUSPENDED
}
}
// 没有挂起点,有未捕获异常
val block3: suspend String.() -> String = {
println("run block3 ,receiver $this")
// 返回以前抛出异常
throwException()
"block 000"
}
// 有挂起点,在第一个挂起点以前有未捕获异常
val block4: suspend String.() -> String = {
// 在第一个挂起点之前就抛出异常
throwException()
// 挂起点1
suspendCoroutineUninterceptedOrReturn<String> {
thread {
println("第一个挂起点resumeWith")
it.resumeWith(Result.success("block 222"))
}
println("第一个挂起点suspended")
COROUTINE_SUSPENDED
}
}
2. 挂起函数/方法引用
步骤:
- 强转为
Function2<String, Continuation<String>, Any?> - 调用
invoke(receiver, completion)
Kotlin
public suspend fun String.getName1(): String {
//.. 省略代码
}
val functionGetName1: suspend String.() -> String = String::getName1
// 反射
functionGetName1::class.java.superclass
// FunctionReferenceImpl
functionGetName1::class.java.interfaces
// "interface kotlin.jvm.functions.Function2"
// "interface kotlin.coroutines.jvm.internal.SuspendFunction"
functionGetName1::class.java.methods
// "public java.lang.Object ...OOODemoKt$main$functionGetName1$1.invoke(java.lang.Object,java.lang.Object)"
// "public final java.lang.Object ...OOODemoKt$main$functionGetName1$1.invoke(java.lang.String,kotlin.coroutines.Continuation)"
functionGetName1 实现了 Function2<P1, P2, R> 接口,如果想要 functionGetName1 as Function2<P1, P2, R>,同样的分析方式可以先得出:
Kotlin
Function2<String, Continuation, Any?>
然后参考代码:
Kotlin
// IntrinsicsJvm.kt
public actual inline fun <T> (suspend () -> T).startCoroutineUninterceptedOrReturn(
completion: Continuation<T>
): Any? =
if (this !is BaseContinuationImpl) wrapWithContinuationImpl(completion)
// 参考这里
else (this as Function1<Continuation<T>, Any?>).invoke(completion)
得出 Function2<String, Continuation<String>, Any?>,所以可以进行强转:
Kotlin
functionGetName1 as Function2<String, Continuation<String>, Any?>
现在我们拿到了 Function2<String, Continuation<String>, Any?>,准备调用它的 invoke(receiver, completion) 方法:
Kotlin
val globleCompletion = object : Continuation<String> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
override fun resumeWith(result: Result<String>) {
println("completion resumeWith $result")
}
}
fun main() {
val functionGetName1: suspend String.() -> String = String::getName1
val receiver = "receiver111"
// 强转
val asFun = (functionGetName1 as Function2<String, Continuation<String>, Any?>)
// 调用invoke
val asFunResult = asFun.invoke(receiver, globleCompletion)
println("asFunResult $asFunResult")
// 输出
// getName1 run! receiver:receiver111, completion == globleCompletion :true
// asFunResult COROUTINE_SUSPENDED
// completion resumeWith Success(getName111)
while (true) {
}
}
public suspend fun String.getName1(): String {
// 在最后一行调用挂起方法,避免getName1内部生成状态机
return suspendCoroutineUninterceptedOrReturn { completion ->
println("getName1 run! receiver:$this, completion == globleCompletion :${completion == globleCompletion}")
thread {
Thread.sleep(2000L)
completion.resumeWith(Result.success("getName111"))
}
COROUTINE_SUSPENDED
}
}
public suspend fun String.getName2(): String {
val result = suspendCoroutineUninterceptedOrReturn { completion ->
println("getName1 run! receiver:$this")
thread {
Thread.sleep(2000L)
completion.resumeWith(Result.success("getName111"))
}
COROUTINE_SUSPENDED
}
return result
}
根据输出可以得知,当调用 invoke(receiver, completion) 时,getName1() 方法中可被挂起的计算就会被执行。打印出的 this 关键字对应的值为 "receiver111",说明 invoke(receiver, completion) 的 receiver 参数指向的值可以在 getName1() 方法内部通过 this 关键字引用。
当 invoke(receiver, completion) 被调用时,依然有如下特性(和前面一样):
- 如果可挂起计算中没有挂起点挂起,并且没有未捕获异常,
invoke()方法直接返回结果,后续completion不会被回调。 - 如果可挂起计算中有挂起点挂起,并且没有未捕获异常,
invoke()方法直接返回COROUTINE_SUSPENDED,后续completion会被回调。 - 如果可挂起计算中没有挂起点挂起,但是有未捕获的异常,这个异常可以被包住
invoke()的 trycatch 块捕获,后续completion不会被回调。 - 如果可挂起计算中有挂起点挂起,但是在执行到第一个挂起点挂起之前有未捕获的异常,这个异常可以被包住
invoke()的 trycatch 块捕获,后续completion不会被回调。
TODO:代码分析为什么有以上四点特性
总之,只有可挂起执行包含至少一个挂起点并且在这个挂起点返回 COROUTINE_SUSPENDED 以后,传递给 invoke 的 completion 才会被回调。
在上面的例子中,getName1() 中的 suspendCoroutineUninterceptedOrReturn 方法内部拿到的续体就直接是 globleCompletion;而方法 getName2() 中 suspendCoroutineUninterceptedOrReturn 方法内部拿到的续体是 getName2() 内部生成的匿名状态机,这个状态机的 completion 指向 globleCompletion。至于为什么会这样,在涉及 invoke() 的这种情况下不知道怎么分析。
另外,也可以将一个非挂起方法的引用赋值给挂起函数类型:
Kotlin
val functionShow :suspend String.() -> String = String::show
public fun String.show():String{
println("run show")
return "show000"
}
分析方式和前面一致。
3. Java 中实现的挂起方法引用
Java
public class SuspendFromJava {
public static Object showWithReceiver(String receiver, Continuation completion) {
Boolean same = completion == OOODemoKt.getGlobleCompletion();
System.out.println("showWithReceiver run ! receiver:" + receiver + " completion == globleCompletion:" + same);
new Thread(() -> {
completion.resumeWith("showWithReceiver000");
}).start();
return CoroutineSingletons.COROUTINE_SUSPENDED;
}
}
Kotlin
fun main() {
// 先赋值给suspend String.() -> String类型
val functionShow: suspend String.() -> String = SuspendFromJava::showWithReceiver as suspend String.() -> String
// val functionShow :suspend String.() -> String =SuspendFromJava::showWithReceiver as suspend String.() -> String
val receiver = "show000Receiver"
//强转
val asFun = functionShow as Function2<String, Continuation<String>, Any?>
// 调用invoke
val asFunResult = asFun.invoke(receiver, globleCompletion)
println("asFunResult $asFunResult")
// 或者直接这样启动showWithReceiver中的可挂起计算
//(SuspendFromJava::showWithReceiver)("123123",globleCompletion)
while (true) {
}
}
分析方式都是一样的,另外:
当 invoke(receiver, completion) 被调用时,依然有如下特性(和前面一样):
- 如果可挂起计算中没有挂起点挂起,并且没有未捕获异常,
invoke()方法直接返回结果,后续completion不会被回调。 - 如果可挂起计算中有挂起点挂起,并且没有未捕获异常,
invoke()方法直接返回COROUTINE_SUSPENDED,后续completion会被回调。 - 如果可挂起计算中没有挂起点挂起,但是有未捕获的异常,这个异常可以被包住
invoke()的 trycatch 块捕获,后续completion不会被回调。 - 如果可挂起计算中有挂起点挂起,但是在执行到第一个挂起点挂起之前有未捕获的异常,这个异常可以被包住
invoke()的 trycatch 块捕获,后续completion不会被回调。
不带 receiver 的版本
分析方式一模一样!
总结
在 kotlin 协程库中实现的启动协程就是启动对应的可被挂起的计算,如果带有 receiver 则在可挂起的计算中可以引用到,当可挂起计算执行结束时,completion 可能会被回调!