今天我们来聊聊Kotlin
的协程Coroutine
。
如果你还没有接触过协程,推荐你先阅读这篇入门级文章What? 你还不知道Kotlin Coroutine?
如果你已经接触过协程,但对协程的原理存在疑惑,那么在阅读本篇文章之前推荐你先阅读下面的文章,这样能让你更全面更顺畅的理解这篇文章。
Kotlin协程实现原理:Suspend&CoroutineContext
Kotlin协程实现原理:CoroutineScope&Job
Kotlin协程实现原理:ContinuationInterceptor&CoroutineDispatcher
如果你已经接触过协程,相信你都有过以下几个疑问:
- 协程到底是个什么东西?
- 协程的
suspend
有什么作用,工作原理是怎样的? - 协程中的一些关键名称(例如:
Job
、Coroutine
、Dispatcher
、CoroutineContext
与CoroutineScope
)它们之间到底是怎么样的关系? - 协程的所谓非阻塞式挂起与恢复又是什么?
- 协程的内部实现原理是怎么样的?
- ...
接下来的一些文章试着来分析一下这些疑问,也欢迎大家一起加入来讨论。
挂起
协程是使用非阻塞式挂起的方式来保证协程运行的。那么什么是非阻塞式挂起呢?下面我们来聊聊挂起到底是一个怎样的操作。
在之前的文章中提及到suspend
关键字,它的一个作用是代码调用的时候会为方法添加一个Continuation
类型的参数,保证协程中Continuaton
的上下传递。
而它另一个关键作用是起到挂起协程的标识。
协程运行的时候每遇到被suspend
修饰的方法时,都有可能会挂起当前的协程。
注意是有可能。
你可以随便写一个方法,该方法也可以被suspend
修饰,但这种方法在协程中调用是不会被挂起的。例如
private suspend fun a() {
println("aa")
}
lifecycleScope.launch {
a()
}
因为这种方法是不会返回COROUTINE_SUSPENDED
类型的。
协程被挂起的标志是对应的状态下返回COROUTINE_SUSPENDED
标识。
更深入一点的话就涉及到状态机。协程内部是使用状态机来管理协程的各个挂起点。
文字有点抽象,具体我们还是来看代码。我们就拿上面的a
方法例子来说明。
首先在Android Studio
打开这段代码的Kotlin Bytecode
。可以在Tools -> Kotlin -> Show Kotlin Bytecode
中打开。
然后点击其中的Decompile
选项,生成对应的反编译java
代码。最终代码如下:
BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
private CoroutineScope p$;
Object L$0;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
// 挂起标识
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
CoroutineScope $this$launch;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
$this$launch = this.p$;
MainActivity var10000 = MainActivity.this;
// 保存现场
this.L$0 = $this$launch;
// 设置挂起后恢复时,进入的状态
this.label = 1;
// 判断是否挂起
if (var10000.a(this) == var3) {
// 挂起,跳出该方法
return var3;
}
// 不需要挂起,协程继续执行其他逻辑
break;
case 1:
// 恢复现场
$this$launch = (CoroutineScope)this.L$0;
// 是否需要抛出异常
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
上面的代码就是协程的状态机,通过label
来代表不同的状态,从而对应执行不同case
中的逻辑代码。
在之前的文章中已经介绍过,协程启动的时候会手动调用一次resumeWith
方法,而它对应的内部逻辑就是执行上面的invokeSuspend
方法。
所以首次运行协程时label
值为0
,进入case 0:
语句。此时会记录现场为可能被挂起的状态做准备,并设置下一个可能被执行的状态。
如果a
方法的返回值为var3
,这个var3
对应的就是COROUTINE_SUSPENDED
。所以只有当a
方法返回COROUTINE_SUSPENDED
时才会执行if
内部语句,跳出方法,此时协程就被挂起。当前线程也就可以执行其它的逻辑,并不会被协程的挂起所阻塞。
所以协程的挂起在代码层面来说就是跳出协程执行的方法体,或者说跳出协程当前状态机下的对应状态,然后等待下一个状态来临时在进行执行。
那为什么说我们写的这个a
方法不会被挂起呢?
@Nullable
final Object a(@NotNull Continuation $completion) {
return Unit.INSTANCE;
}
原来是它的返回值并不是COROUTINE_SUSPENDED
。
既然它不会被挂起,那么什么情况下的方法才会被挂起呢?
很简单,如果我们在a
方法中加入delay
方法,它就会被挂起。
@Nullable
final Object a(@NotNull Continuation $completion) {
Object var10000 = DelayKt.delay(1000L, $completion);
return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}
真正触发挂起的是delay
方法,因为delay
方法会创建自己Continuation
,同时内部调用getResult
方法。
internal fun getResult(): Any? {
installParentCancellationHandler()
if (trySuspend()) return COROUTINE_SUSPENDED
// otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state
val state = this.state
if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
return getSuccessfulResult(state)
}
在getResult
方法中会通过trySuspend
来判断挂起当前协程。由挂起自身的协程,从而触发挂起父类的协程。
如果只是为了测试,可以让a
方法直接返回COROUTINE_SUSPENDED
private suspend fun a(): Any {
return kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
}
当然线上千万不能这样写,因为一旦这样写协程将一直被挂起,因为你没有将其恢复的能力。
恢复
现在我们再来聊一聊协程的恢复。
协程的恢复本质是通过Continuation
的resumeWith
方法来触发的。
下面我们来看一个可以挂起的例子,通过它来分析协程挂起与恢复的整个流程。
代码语言:javascript复制println("main start")
lifecycleScope.launch {
println("async start")
val b = async {
delay(2000)
"async"
}
b.await()
println("async end")
}
Handler().postDelayed({
println("main end")
}, 1000)
Kotlin
代码很简单,当前协程运行与主线程中,内部执行一个async
方法,通过await
方法触发协程的挂起。
再来看它的对应反编译java
代码
// 1
String var2 = "main start";
System.out.println(var2);
BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
private CoroutineScope p$;
Object L$0;
Object L$1;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
CoroutineScope $this$launch;
Deferred b;
switch(this.label) {
case 0:
// 2
ResultKt.throwOnFailure($result);
$this$launch = this.p$;
String var6 = "async start";
System.out.println(var6);
b = BuildersKt.async$default($this$launch, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
private CoroutineScope p$;
Object L$0;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
CoroutineScope $this$async;
switch(this.label) {
case 0:
// 3
ResultKt.throwOnFailure($result);
$this$async = this.p$;
this.L$0 = $this$async;
this.label = 1;
if (DelayKt.delay(2000L, this) == var3) {
return var3;
}
break;
case 1:
// 5、6
$this$async = (CoroutineScope)this.L$0;
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return "async";
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
this.L$0 = $this$launch;
this.L$1 = b;
this.label = 1;
if (b.await(this) == var5) {
return var5;
}
break;
case 1:
// 7
b = (Deferred)this.L$1;
$this$launch = (CoroutineScope)this.L$0;
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
// 8
String var4 = "async end";
System.out.println(var4);
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
// 4
(new Handler()).postDelayed((Runnable)null.INSTANCE, 1000L);
有点长,没关系我们只看关键点,看它的状态机相关的内容。
- 首先会输出
main start
,然后通过launch
创建协程,进入协程状态机,此时label
为0
,执行case: 0
相关逻辑。 - 进入
case: 0
后输出async start
,调用async
并通过await
来挂起当前协程,再挂起的过程中记录当前挂起点的数据,并将lable
设置为1
。 - 进入
async
创建的协程,此时async
协程中的lable
为0
,进入async case: 0
执行dealy
并挂起async
的协程。并将label
设置为1
。等待2s
之后被唤醒。 - 此时协程都被挂起,即跳出协程
launch
方法,执行handler
操作。由于post 1s
所以比协程中dealy
还短,所以会优先输出main end
,然后再过1s
,进入恢复协程阶段 async
中的协程被delay
恢复,注意在delay
方法中传入了this
,async
的Continuation
对象,所以delay
内部一旦完成2s
计时就会调用Continuation
的resumeWith
方法来恢复async
中的协程,即调用invokeSuspend
方法。- 由于被挂起之前已经将
async label
设置为1
,所以进入case: 1
,恢复之前挂起的现场,检查异常,最终返回async
。 - 此时
await
挂起点被恢复,注意它也传入了this
,对应的就是launch
中的Continuation
,所以也会回调resumeWith
方法,最终调用invokeSuspend
,即进入case 1:
恢复现场,结束状态机。 - 最后再继续输出
async end
,协程运行结束。
我们可以执行上面的代码来验证输出是否正确
代码语言:javascript复制main start
async start
main end
async end
我们来总结一下,协程通过suspend
来标识挂起点,但真正的挂起点还需要通过是否返回COROUTINE_SUSPENDED
来判断,而代码体现是通过状态机来处理协程的挂起与恢复。在需要挂起的时候,先保留现场与设置下一个状态点,然后再通过退出方法的方式来挂起协程。在挂起的过程中并不会阻塞当前的线程。对应的恢复通过resumeWith
来进入状态机的下一个状态,同时在进入下一个状态时会恢复之前挂起的现场。
本篇文章主要介绍了协程的挂起与恢复原理,同时也分析了协程的状态机相关的执行过程。希望对学习协程的伙伴们能够有所帮助,敬请期待后续的协程分析。
项目
android_startup: https://github.com/idisfkj/android-startup
提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup
的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。
AwesomeGithub:https://github.com/idisfkj/AwesomeGithub
基于Github
客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin
语言进行开发,项目架构是基于Jetpack&DataBinding
的MVVM
;项目中使用了Arouter
、Retrofit
、Coroutine
、Glide
、Dagger
与Hilt
等流行开源技术。
flutter_github: https://github.com/idisfkj/flutter_github
基于Flutter
的跨平台版本Github
客户端,与AwesomeGithub
相对应。
android-api-analysis: https://github.com/idisfkj/android-api-analysis
结合详细的Demo
来全面解析Android
相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。
daily_algorithm: https://github.com/idisfkj/daily_algorithm
每日一算法,由浅入深,欢迎加入一起共勉。