前言
协程系列文章:
- 一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?
- 少年,你可知 Kotlin 协程最初的样子?
- 讲真,Kotlin 协程的挂起/恢复没那么神秘(故事篇)
- 讲真,Kotlin 协程的挂起/恢复没那么神秘(原理篇)
- Kotlin 协程调度切换线程是时候解开真相了
- Kotlin 协程之线程池探索之旅(与Java线程池PK)
- Kotlin 协程之取消与异常处理探索之旅(上)
- Kotlin 协程之取消与异常处理探索之旅(下)
- 来,跟我一起撸Kotlin runBlocking/launch/join/async/delay 原理&使用
如果有人问你,怎么开启一个 Kotlin 协程?你可能会说通过runBlocking/launch/async,回答没错,这几个函数都能开启协程。不过这次咱们换个角度分析,通过提取这几个函数的共性,看看他们内部是怎么开启一个协程的。
相信通过本篇,你将对协程原理有个深刻的认识。
文章目录:
1、suspend 关键字背后的原理
2、如何开启一个原始的协程?
3、协程调用以及整体流程
4、协程代码我为啥看不懂?
1、suspend 关键字背后的原理
suspend 修饰函数
普通的函数
fun launchEmpty(block: () -> Unit) {
}
定义一个函数,形参为函数类型。
查看反编译结果:
public final class CoroutineRawKt {
public static final void launchEmpty(@NotNull Function0 block) {
}
}
可以看出,在JVM 平台函数类型参数最终是用匿名内部类表示的,而FunctionX(X=0~22) 是Kotlin 将函数类型映射为Java 的接口。
来看看Function0 的定义:
public interface Function0 : Function {
/** Invokes the function. */
public operator fun invoke(): R
}
有一个唯一的方法:invoke(),它没有任何参数。
可作如下调用:
fun launchEmpty(block: () -> Unit) {
block()//与block.invoke()等价
}
fun main(array: Array) {
launchEmpty {
println("I am empty")
}
}
带suspend 的函数
以上写法大家都比较熟悉了,就是典型的高阶函数的定义和调用。
现在来改造一下函数类型的修饰符:
fun launchEmpty1(block: suspend () -> Unit) {
}
相较之前,加了"suspend"关键字。
老规矩,查看反编译结果:
public static final void launchEmpty1(@NotNull Function1 block) {
}
参数从Function0 变为了Function1:
/** A function that takes 1 argument. */
public interface Function1 : Function {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
Function1 的invoke()函数多了一个入参。
也就是说,加了suspend 修饰后,函数会默认加个形参。
当我们调用suspend修饰的函数时:
意思是:
"suspend"修饰的函数只能在协程里被调用或者是在另一个被"suspend"修饰的函数里调用。
suspend 作用
何为挂起
suspend 意为挂起、阻塞的意思,与协程相关。
当suspend 修饰函数时,表明这个函数可能会被挂起,至于是否被挂起取决于该函数里是否有挂起动作。
比如:
suspend fun testSuspend() {
println("test suspend")
}
这样的写法没意义,因为函数没有实现挂起功能。
你可能会说,挂起需要切换线程,好嘛,换个写法:
suspend fun testSuspend() {
println("test suspend")
thread {
println("test suspend in thread")
}
}
然而并没啥用,编译器依然提示:
意思是可以不用suspend 修饰,没啥意义。
挂起于协程的意义
第一点
当函数被suspend 修饰时,表明协程执行到此可能会被挂起,若是被挂起那么意味着协程将无法再继续往下执行,直到条件满足恢复了协程的运行。
fun main(array: Array) {
GlobalScope.launch {
println("before suspend")//①
testSuspend()//挂起函数②
println("after suspend")//③
}
}
执行到②时,协程被挂起,将不会执行③,直到协程被恢复后才会执行③。
注:关于协程挂起的生动理解&线程的挂起 下篇将着重分析。
第二点
如果将suspend 修饰的函数类型看做一个整体的话:
suspend () -> T
无参,返回值为泛型。
Kotlin 里定义了一些扩展函数,可用来开启协程。
第三点
suspend 修饰的函数类型,当调用者实现其函数体时,传入的实参将会继承自SuspendLambda(这块下个小结详细分析)。
2、如何开启一个原始的协程?
launch/async/runBlocking 如何开启协程
纵观这几种主流的开启协程方式,它们最终都会调用到:
#CoroutineStart.kt
public operator fun invoke(block: suspend R.() -> T, receiver: R, completion: Continuation): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(receiver, completion)
ATOMIC -> block.startCoroutine(receiver, completion)
UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
LAZY -> Unit // will start lazily
}
无论走哪个分支,都是调用block的函数,而block 就是我们之前说的被suspend 修饰的函数。
以DEFAULT 为例startCoroutineUndispatched接下来会调用到IntrinsicsJvm.kt里的:
#IntrinsicsJvm.kt
public actual fun (suspend R.() -> T).createCoroutineUnintercepted(
receiver: R,
completion: Continuation
)
该函数带了俩参数,其中的receiver 为接收者,而completion 为协程结束后调用的回调。
为了简单,我们可以省略掉receiver。
刚好IntrinsicsJvm.kt 里还有另一个函数:
#IntrinsicsJvm.kt
public actual fun (suspend () -> T).createCoroutineUnintercepted(
completion: Continuation
): Continuation
createCoroutineUnintercepted 为 (suspend () -> T) 类型的扩展函数,因此只要我们的变量为 (suspend () -> T)类型就可以调用createCoroutineUnintercepted(xx)函数。
查找该函数的使用之处,发现Continuation.kt 文件里不少扩展函数都调用了它。
如:
#Continuation.kt
//创建协程的函数
public fun (suspend () -> T).createCoroutine(
completion: Continuation
): Continuation =
SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
其中Continuation 为接口:
#Continuation.kt
interface Continuation {
//协程上下文
public val context: CoroutineContext
//恢复协程
public fun resumeWith(result: Result)
}
Continuation 接口很重要,协程里大部分的类都实现了该接口,通常直译过来为:"续体"。
创建完成后,还需要开启协程函数:
#Continuation.kt
//启动协程的函数
public inline fun Continuation.resume(value: T): Unit =
resumeWith(Result.success(value))
简单创建/调用协程
协程创建
由上分析可知,Continuation.kt 里有我们开启协程所需要的一些基本信息,接着来看看如何调用上述函数。
fun launchFish(block: suspend () -> T) {
//创建协程,返回值为SafeContinuation(实现了Continuation 接口)
//入参为Continuation 类型,参数名为completion,顾名思义就是
//协程结束后(正常返回&抛出异常)将会调用它。
var coroutine = block.createCoroutine(object : Continuation {
override val context: CoroutineContext
get() = EmptyCoroutineContext
//协程结束后调用该函数
override fun resumeWith(result: Result) {
println("result:$result")
}
})
//开启协程
coroutine.resume(Unit)
}
定义了函数launchFish,该函数唯一的参数为函数类型参数,被suspend 修饰,而(suspend () -> T)定义一系列扩展函数,createCoroutine 为其中之一,因此block 可以调用createCoroutine。
createCoroutine 返回类型为SafeContinuation,通过SafeContinuation.resume()开启协程。
协程调用
fun main(array: Array) {
launchFish {
println("I am coroutine")
}
}
打印结果:
3、协程调用以及整体流程
协程调用背后的玄机
反编译初窥门径
看到上面的打印大家可能比较晕,"println("I am coroutine")"是咋就被调用的?没看到有调用它的地方啊。
launchFish(block) 接收的是函数类型,当调用launchFish 时,在闭包里实现该函数的函数体即可,我们知道函数类型最终会替换为匿名内部类。
因为kotlin 有不少语法糖,无法一下子直击本质,老规矩,反编译看看结果:
public static final void main(@NotNull String[] array) {
launchFish((Function1)(new Function1((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object var1) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
//闭包里的内容
String var2 = "I am coroutine";
boolean var3 = false;
//打印
System.out.println(var2);
return Unit.INSTANCE;
}
}
@NotNull
public final Continuation create(@NotNull Continuation completion) {
//创建一个Continuation,可以认为是续体
Function1 var2 = new (completion);
return var2;
}
public final Object invoke(Object var1) {
//Function1 接口里的方法
return (()this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
}
}));
}
为了更直观,删除了一些不必要的信息。
看到这,你发现了什么?通常传入函数类型的实参最后将会被编译为对应的匿名内部类,此时应该编译为Function1,实现其唯一的函数:invoke(xx),而我们发现实际上还多了两个函数:invokeSuspend(xx)与create(xx)。
我们有理由相信,invokeSuspend(xx)函数一定在某个地方被调用了,原因是:闭包里打印的字符串:"I am coroutine" 只在该函数里实现,而我们测试的结果是这个打印执行了。
还记得我们上面说的suspend 意义的第三点吗?
suspend 修饰的函数类型,其实参是匿名内部类,继承自抽象类:SuspendLambda。
也就是说invokeSuspend(xx)与create(xx) 的定义很有可能来自SuspendLambda,我们接着来分析它。
SuspendLambda 关系链
#ContinuationImpl.kt
internal abstract class SuspendLambda(
public override val arity: Int,
completion: Continuation?
) : ContinuationImpl(completion), FunctionBase, SuspendFunction {
constructor(arity: Int) : this(arity, null)
...
}
该类本身并没有太多内容,此处继承了ContinuationImpl类,查看该类也没啥特殊的,继续往上查找,找到BaseContinuationImpl类,在里面发现了线索:
#ContinuationImpl.kt
internal abstract class BaseContinuationImpl(
val completion: Continuation?
) : Continuation, CoroutineStackFrame, Serializable {
protected abstract fun invokeSuspend(result: Result): Any?
open fun create(completion: Continuation<*>): Continuation {
}
}
终于看到了眼熟的:invokeSuspend(xx)与create(xx)。
我们再回过头来捋一下类之间关系:
闭包生成的匿名内部类:
- 实现了Function1 接口,并实现了该接口里的invoke函数。
- 继承了SuspendLambda,并重写了invokeSuspend函数和create函数。
你可能会说还不够直观,那好,继续改写一下:
class MyAnonymous extends SuspendLambda implements Function1 {
int label;
public final Object invokeSuspend(@NotNull Object var1) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
String var2 = "I am coroutine";
boolean var3 = false;
System.out.println(var2);
return Unit.INSTANCE;
}
}
public final Continuation create(@NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function1 var2 = new (completion);
return var2;
}
public final Object invoke(Object var1) {
return (()this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
}
}
public static final void launchFish(@NotNull MyAnonymous block) {
Continuation coroutine = ContinuationKt.createCoroutine(block, (new Continuation() {
@NotNull
public CoroutineContext getContext() {
return (CoroutineContext) EmptyCoroutineContext.INSTANCE;
}
public void resumeWith(@NotNull Object result) {
String var2 = "result:" + Result.toString-impl(result);
boolean var3 = false;
System.out.println(var2);
}
}));
//开启
coroutine.resumeWith(Result.constructor-impl(var3));
}
public static final void main(@NotNull String[] array) {
MyAnonymous myAnonymous = new MyAnonymous();
launchFish(myAnonymous);
}
这么看就比较清晰了,此处我们单独声明了一个MyAnonymous类,并构造对象传递给launchFish函数。
闭包的执行
既然匿名类的构造清晰了,接下来分析闭包是如何被执行的,也就是查找invokeSuspend(xx)函数是怎么被调用的?
将目光转移到launchFish 函数本身。
createCoroutine()
先看createCoroutine()函数调用,直接上代码:
#Continuation.kt
fun (suspend () -> T).createCoroutine(
completion: Continuation
): Continuation =
//返回SafeContinuation 对象
//SafeContinuation 构造函数需要2个参数,一个是delegate,另一个是协程状态
//此处默认是挂起
SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
#IntrinsicsJvm.kt
actual fun (suspend () -> T).createCoroutineUnintercepted(
completion: Continuation
): Continuation {
val probeCompletion = probeCoroutineCreated(completion)
return if (this is BaseContinuationImpl)
//此处的this 即为匿名内部类对象 MyAnonymous,它间接继承了BaseContinuationImpl
//调用MyAnonymous 重写的create 函数
//create 函数里new 新的MyAnonymous 对象
create(probeCompletion)
else
createCoroutineFromSuspendFunction(probeCompletion) {
(this as Function1, Any?>).invoke(it)
}
}
#IntrinsicsJvm.kt
public actual fun Continuation.intercepted(): Continuation =
//判断是否是ContinuationImpl 类型的Continuation
//我们的demo里是true,因此会继续尝试调用拦截器
(this as? ContinuationImpl)?.intercepted() ?: this
#ContinuationImpl.kt
public fun intercepted(): Continuation =
//查看是否已经有拦截器,如果没有,则从上下文里找,上下文没有,则用自身,最后赋值。
//在我们的demo里上下文里没有,用的是自身
intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }
最后得出的Continuation 赋值给SafeContinuation 的成员变量:delegate。
至此,SafeContinuation 对象已经构造完毕,接着继续看如何用它开启协程。
再看 resume()
#SafeContinuationJvm.kt
actual override fun resumeWith(result: Result) {
while (true) { // lock-free loop
val cur = this.result // atomic read
when {
//初始化状态为UNDECIDED,因此直接return
cur === CoroutineSingletons.UNDECIDED -> if (SafeContinuation.RESULT.compareAndSet(this,
CoroutineSingletons.UNDECIDED, result.value)) return
//如果是挂起,将它变为恢复状态,并调用恢复函数
//demo 里初始化状态为COROUTINE_SUSPENDED,因此会走到这
cur === COROUTINE_SUSPENDED -> if (SafeContinuation.RESULT.compareAndSet(this, COROUTINE_SUSPENDED,
CoroutineSingletons.RESUMED)) {
//delegate 为之前创建的Continuation,demo 里因为没有拦截,因此为MyAnonymous
delegate.resumeWith(result)
return
}
else -> throw IllegalStateException("Already resumed")
}
}
}
#ContinuationImpl.kotlin
#BaseContinuationImpl类的成员函数
override fun resumeWith(result: Result) {
var current = this
var param = result
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!!
val outcome: Result =
try {
//invokeSuspend 即为MyAnonymous 里的方法
val outcome = invokeSuspend(param)
//如果返回值是挂起状态,则函数直接退出
if (outcome === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) return
kotlin.Result.success(outcome)
} catch (exception: Throwable) {
kotlin.Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
current = completion
param = outcome
} else {
//执行到这,最终执行外层的completion,在demo里会输出"result:$result"
completion.resumeWith(outcome)
return
}
}
}
}
最后再回头看 invokeSuspend
public final Object invokeSuspend(@NotNull Object var1) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure(var1);
String var2 = "I am coroutine";
boolean var3 = false;
System.out.println(var2);
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
你兴许已经发现了,此处的返回值永远是Unit.INSTANCE啊,那么协程永远不会挂起。
没有挂起功能的协程就是鸡肋...。
没错,咱们的demo里实现的是一个无法挂起的协程,回到最初的launchFish()的调用:
launchFish {
println("I am coroutine")
}
}
因为闭包里只有一个打印语句,根本没有挂起函数,当然就没有挂起的说法了。
协程调用整体流程
上面花很多篇幅去分析协程的调用,其实就是为了从kotlin 的简洁里脱离出来,从而真正了解其背后的原理。
Demo里的协程构造比较原始,相较于launch/async 等启动方式,它没有上下文、没有线程调度,但并不妨碍我们通过它去了解协程的运作。当我们了解了其运作的核心,到时候再去看launch/async/runBlocking 就非常容易了,毕竟它们都是提供给开发者更方便操作协程的工具,是在原始携程的基础上演变的。
协程创建调用栈简易图:
4、协程代码我为啥看不懂?
之前有一些小伙伴跟我反馈说:"小鱼人,我尝试去看协程源码,感觉找不到入口,又或是跟着源码跟到一半就断了...你是咋阅读的啊?"
有一说一,协程源码确实不太好懂,若要比较顺畅读懂源码,根据个人经验可能需要以下前置条件:
1、kotlin 语法基础,这是必须的。
2、高阶函数&扩展函数。
3、平台代码差异,有一些类、函数是与平台相关,需要定位到具体平台,比如SafeContinuation,找到Java 平台的文件:SafeContinuationJvm.kt。
4、断点调试时,有些单步断点不会进入,需要指定运行到的位置。
5、有些代码是编译时期构造的,需要对照反编译结果查看。
6、还有些代码是没有源码的,可能是ASM插入的,此时只能靠肉眼理解了。
如果你对kotlin 基础/高阶函数 等有疑惑,请查看之前的文章。
本篇仅仅构造了一个简陋的协程,协程的最重要的挂起/恢复并没有涉及,下篇将会着重分析如何构造一个挂起函数,以及协程到底是怎么挂起的。
本文基于Kotlin 1.5.3,文中完整Demo请点击
您若喜欢,请点赞、关注,您的鼓励是我前进的动力
持续更新中,和我一起步步为营系统、深入学习Android/Kotlin
1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易懂易学系列
19、Kotlin 轻松入门系列