
今天我們來聊聊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;@Nullablepublic 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;}@NotNullpublic 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 stateval state = this.stateif (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
方法來觸發的。
下面我們來看一個可以掛起的例子,通過它來分析協程掛起與恢復的整個流程。
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;@Nullablepublic final Object invokeSuspend(@NotNull Object $result) {Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();CoroutineScope $this$launch;Deferred b;switch(this.label) {case 0:// 2ResultKt.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;@Nullablepublic final Object invokeSuspend(@NotNull Object $result) {Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();CoroutineScope $this$async;switch(this.label) {case 0:// 3ResultKt.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";}@NotNullpublic 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:// 7b = (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");}// 8String var4 = "async end";System.out.println(var4);return Unit.INSTANCE;}@NotNullpublic 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
,協程運行結束。
我們可以執行上面的代碼來驗證輸出是否正確
main start
async start
main end
async end
我們來總結一下,協程通過suspend
來標識掛起點,但真正的掛起點還需要通過是否返回COROUTINE_SUSPENDED
來判斷,而代碼體現是通過狀態機來處理協程的掛起與恢復。在需要掛起的時候,先保留現場與設置下一個狀態點,然后再通過退出方法的方式來掛起協程。在掛起的過程中并不會阻塞當前的線程。對應的恢復通過resumeWith
來進入狀態機的下一個狀態,同時在進入下一個狀態時會恢復之前掛起的現場。
本篇文章主要介紹了協程的掛起與恢復原理,同時也分析了協程的狀態機相關的執行過程。希望對學習協程的伙伴們能夠有所幫助,敬請期待后續的協程分析。
項目
android_startup: 提供一種在應用啟動時能夠更加簡單、高效的方式來初始化組件,優化啟動速度。不僅支持Jetpack App Startup
的全部功能,還提供額外的同步與異步等待、線程控制與多進程支持等功能。
AwesomeGithub: 基于Github
客戶端,純練習項目,支持組件化開發,支持賬戶密碼與認證登陸。使用Kotlin
語言進行開發,項目架構是基于Jetpack&DataBinding
的MVVM
;項目中使用了Arouter
、Retrofit
、Coroutine
、Glide
、Dagger
與Hilt
等流行開源技術。
flutter_github: 基于Flutter
的跨平臺版本Github
客戶端,與AwesomeGithub
相對應。
android-api-analysis: 結合詳細的Demo
來全面解析Android
相關的知識點, 幫助讀者能夠更快的掌握與理解所闡述的要點。
daily_algorithm: 每日一算法,由淺入深,歡迎加入一起共勉。