Kotlin Coroutines 幕後那一兩件事

Jast Lai
Jastzeonic
Published in
35 min readApr 2, 2020

前言

Coroutines 是近幾年在 Kotlin 上 Google 主推的異步問題解決方案,至少在 Android R Asynctask 被 deprecate 後,打開 Android Document 看到最顯目的提示項目就是導引你至 Coroutine 的頁面教導你怎麼使用 Coroutine。

Emm….那如果把所有問題簡單化,其實大多數碰上異步問題,解決的辦法基本上都是 callback。

fun longTimeProcess(parameter1 : String, callBack:CallBack<String>){    val result = ""
//Do something long time
callBack.onResponse(result)
}

其實東西寫好放在那邊給人呼叫,本質上都是 callback 就是了,所以很多異步解決方案,追到最後的本質,會發現他的實作方式仍舊是 callback。

不過 callback 的使用情境、context 還有許許多多的用法情況都不同,整體概念也會有出入,所以我們會需要更多的名詞來代表這樣的情況,因此延伸出更多樣的詞彙,不過這段就題外話了。

話說回來,上面那段簡易的 callback ,換作是 Coroutine 會變成是這樣:

suspend fun longTimeProcess(parameter1 : String):String{
val result = ""
//Do something long time
return result
}

換成是這樣寫的好處是可以不用自己控制 Thread 的使用,上面那個在 Android 上頭,如果直接在 main thread 呼叫,可能會造成卡住 main thread 超過 5 秒噴 Exception 直接讓 Process out,所以還會需要額外自己開 thread + handler 或是使用 Rxjava 之類第三方套件去處理。換作是 Coroutine ,使用起來就簡單很多了,寫好可以被 suspend 的 function ,找個屬於他的 Scope,用 scope launch 裏頭呼叫該 function,利用這個 function 回傳的資料做該在 main thread 上解決的事情,問題解決,就是如此的簡單,如此的美好。

那問題來了。

Coroutine 到底是怎麼運作的?究竟是甚麼神奇的魔法讓他可以這麼的方便可以不用寫那麼多東西呢?

嘛,記得某次面試裡有提到這個問題,但我只知道他是個有限狀態機,然後就…

恩,我那時的表情應該跟 King Crimson 有那麼幾分神似就是了。

先講個簡單的概念

維基百科上其實有解釋了 Coroutine 的實作概念:

var q := new queuecoroutine produce
loop
while q is not full
create some new items
add the items to q
yield to consume

coroutine consume
loop
while q is not empty
remove some items from q
use the items
yield to produce

概念是,這有個 queue 是空的,那是先跑 coroutine product 還是 coroutine consume 其實無所謂,總之隨便跑一個,先從 coroutine product 開始好了。

coroutine produce 在 queue 沒滿時,會產生一些 items ,然後加入 queue 裏頭,直到 queue 滿為止,接著把程序讓給 coroutine consume。

coroutine consume 在 queue 不是空的時候, 會移除(消費)一些 items ,直到 queue 空為止,接著把程序讓給 coroutine produce,如此反覆,這個世界的經濟得以維持。

那這邊可以看出,當 coroutine produce 碰到 queue 是滿的時候會直接把程序讓給 coroutine consume ;相對的,若 coroutine consume 在碰到 queue 是空的時候,會直接把程序讓給 coroutine produce 。

那麼,以 Kotlin Coroutine 來說,queue 的是空是滿的條件會變成是 method 的狀態是否 suspend,那因為上面這個程序很明顯會是無限迴圈,多數我們在開發時會不需要無限的迴圈,那怎麼樣才能讓這種來回傳接球的形式有個終點呢?

答案就是有限狀態機,接下來這篇文章會慢慢地解釋。

有這麼個東西叫做 Continuation

嘛,很多時候,原本很麻煩的事情突然變得簡單了,其實結果不是事情變得不用做,而是事情有人幫你做了,Coroutine 也是,在 Kotlin ,他叫 Kotlin Compiler ,他幫你把寫一堆 callback 的麻煩事給做掉了。

等等,Compiler 把寫一堆的 callback 的麻煩事給做掉了,那意思是…

沒錯, Coroutine 本質上還是 callback,只是 compiler 幫你把他寫掉了。Well ,結果我用了 Coroutine 還是在寫 callback 阿淦。

我本來是想說從 CoroutineScope.Launch 下去追的,追到 IntrinsicsJvm,這東西叫 Intrinsic 這東西有很大的機率是給 compiler 用的,追到這裡,大概就可以知道,Suspend fun 會在 compiler 的過程轉成 Continuation ,只是除非去翻 Kotlin 的 Compiler,否則會很難找到關鍵的證據。

但後來換個方向去想,其實也不用這麼麻煩,因為 Kotlin 是可以給 Java 呼叫的,那 Java 比較少這種語法糖轉譯的東西,也就是說,透過 Java 呼叫 suspend fun ,就可以知道 suspend fun 真正的模樣。

這邊先隨便寫一個 suspend fun。

suspend fun getUserDescription(name:String,id:Int):String{
return ""
}

在 Java 呼叫的時候會長這樣:

instance.getUserDescription("name", 0, new Continuation<String>() {
@NotNull
@Override
public CoroutineContext getContext() {
return null;
}

@Override
public void resumeWith(@NotNull Object o) {

}
});
return 0;

先別管中間長怎樣,反正這樣叫肯定會噴錯(好像會噴 NullPointerException吧),反正到這邊知道了,其實 suspend fun 就是一般的 function 後頭加上一個 Continuation。

總之得到一個線索,這個線索就是 Continuation ,它是個什麼玩意呢?

它是一個 interface,不過開發者本身不太需要用到它(要用也是可以,但是塞 parameter 很麻煩),它是給 compiler 用的。

public interface Continuation<in T> {
public val context: CoroutineContext

public fun resumeWith(result: Result<T>)
}

它代表的是 Coroutine 的 runBlock 在 suspend 狀態中,要被喚醒的 callback。

那注意這邊提到狀態了,大夥都知道 Coroutine 會是個狀態機,那具體是甚麼狀態呢?這個稍後提。

那如果硬要在 java file 裏頭使用 GlobalScope.launch,那會長成這樣:

BuildersKt.launch(GlobalScope.INSTANCE,
Dispatchers.getMain(),//context to be ran on
CoroutineStart.DEFAULT,
new Function2<CoroutineScope,Continuation<? super Unit>,String>() {
@Override
public String invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {

return "";
}
}
);

這樣就行了嗎 ? 這樣好像沒啥效果最後會回一個空字串就是了,但這裡就會發現,如果用 lanuch 會需要用到一個 Function 去傳遞一個 continuation。這樣看還是霧傻傻,沒關係,咱們繼續看下去。

那用 Continuation 到底怎麼跑

那這邊簡單用一個 suspend :

fun main() {
GlobalScope.launch {
val text = suspendFunction("text")
println(text) // print after delay
}

}

suspend fun suspendFunction(text:String) = withContext(Dispatchers.IO){

val result = doSomethingLongTimeProcess(text)
result
}

那用 Kotlin Bytecode 去 decompile 會得到這個:

public static final void main() {
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (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 var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Object var10000;
CoroutineScope $this$launch;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
$this$launch = this.p$;
this.L$0 = $this$launch;
this.label = 1;
var10000 = CoroutineTestKt.suspendFunction("text", this);
if (var10000 == var5) {
return var5;
}
break;
case 1:
$this$launch = (CoroutineScope)this.L$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

String text = (String)var10000;
boolean var4 = false;
System.out.println(text);
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);
}

另外這個是 suspendFunction的 decompile code:

public static final Object suspendFunction(@NotNull final String text, @NotNull Continuation $completion) {
return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
private CoroutineScope p$;
int label;

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
CoroutineScope $this$withContext = this.p$;
String result = CoroutineTestKt.doSomethingLongTimeProcess(text);
return result;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}

@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);
}
}), $completion);
}

這個其實是很 ByteCode 的 java ,屬於有點看得懂又有點看不懂的程度,用眼睛看會看得眼睛很痛,但是找人唸出來大概也不知道他在說啥咒文,所以到頭來還是得把眼睛看的亂疼一把的,沒關係,眼睛我痛就好,如果你沒看到這段眼睛已經發痛那沒關係咱們一起痛吧(咦?),我這邊直接解釋一下流程。

中間會看到一個 switch(this.label) , 這就是 Coroutine 的狀態機了,Kotlin Compiler 會在編譯的時候產生一個 label ,這個就是 runBlock 裡面執行到第幾階段的狀態了,會有幾個狀態呢?在 runBlock 裡面有幾個 suspend 就會有幾個狀態,舉個例子:

GlobalScope.launch {
test()
test()
test()
test()
}
fun test(){}

這樣會有幾個?

答案是一個,因為這 test() 不是 suspend function,它不需要 suspended 。

如果換成是這樣?

GlobalScope.launch {
test()
test()
test()
test()
}
suspend fun test(){}

答案是五個。

GlobalScope.launch {
// case 0
test() // case 1 receive result
test() // case 2 receive result
test() // case 3 receive result
test() // case 4 receive result
}

因為四個 test() 都有可能進到 suspended 的狀態,所以會需要五個執行狀態 case 0 做初始化,case 1– 4 作結果的收取。

那狀態何時會改變呢 ? invokeSuspend 被呼叫的時候。

label34: {
label33: {
var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
$this$launch = this.p$;
this.L$0 = $this$launch;
this.label = 1;
if (CoroutineTestKt.test(this) == var3) {
return var3;
}
break;
case 1://...ignore
break;
case 2://...ignore
break label33;
case 3://...ignore
break label34;
case 4://...ignore
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
this.L$0 = $this$launch;
this.label = 2;
if (CoroutineTestKt.test(this) == var3) {
return var3;
}
}

this.L$0 = $this$launch;
this.label = 3;
if (CoroutineTestKt.test(this) == var3) {
return var3;
}
}

this.L$0 = $this$launch;
this.label = 4;
if (CoroutineTestKt.test(this) == var3) {
return var3;
} else {
return Unit.INSTANCE;
}

這部分比較有意思的地方是,這些狀態還有 call method 的都不在 switch case 裡面,這其實跟 Bytecode 有關就是了,主要是因為這個結果是 decompile 出來的東西,所以會是這樣疊加的方式。

那這邊可以觀察出來,在狀態改變後,呼叫 suspend function 的地方:

Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();//...ignore

this.label = 1;
if (CoroutineTestKt.test(this) == var3) {
return var3;
}

可以發現 call 該 method 它會回傳一個狀態(至於該 method 若有回傳結果則也有可能回傳該結果),它有可能是 IntrinsicsKt.getCOROUTINE_SUSPENDED() ,這代表了該 function 現行的狀態,也就是如果我 sampleSuspendFunction 回傳不是 IntrinsicsKt.getCOROUTINE_SUSPENDED() 代表它的狀態已經不是 suspended ,可以直接進到下一步;那如果它回傳的是 getCOROUTINE_SUSPENDED,代表這個 function 處在 suspended 的狀態,意味著它可能還在進行耗時作業還沒做完,這時候就直接 suspended 狀態,等待下一次被 invoke。

那何時會再一次被 invoke 呢?

這時候就要看到傳入該 suspend function 的 Continuation ,這裡可以觀察一下 BaseContinuationImpl 的 resumeWith 的實作:

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!!
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted()
if (completion is BaseContinuationImpl) {
current = completion
param = outcome
} else {
completion.resumeWith(outcome)
return
}
}
}
}
//...ignore
}

原則上 resumeWith 在一開始 Coroutine 被創建時就會呼叫到(所以需要 case 0 做初始化),那可以看到開頭沒做啥 invokeSuspend 被呼叫到了(probeCoroutineResumed 那個看起來是 debug 用的請無視),很自然的就執行了 invokeSuspend 開始跑那台狀態機了,如果該 continuation 的狀態是 suspended ,那就會結束,等下一次被喚醒,再次被喚醒後,在這可以得到結果丟給該 suspend function 的 continuation (name in completion),然後結束掉,接著到該 suspend function 的 invokeSuspend ,如此反覆直到結束。

PS:Android Studio 的 decompile 會有無限 method 的問題,所以這邊是用 IntelliJ IDE 去解編的,有興趣可以參考

到這裡,我們知道了, suspend 就是被賦予 function 狀態讓它可以 suspended ,且利用狀態機來記錄 Coroutine 執行的狀態。所以我們在 invoke 該 function 的時候可以得到它的狀態是 getCOROUTINE_SUSPENDED,因此 suspend Coroutine ,待到下次被喚醒再從 Coroutine 被 suspend 的地方繼續執行。

不過有趣的問題來了,那既然 suspend function return 了狀態,代表它已經結束了,那它是怎麼繼續執行?而且還有辦法再執行完後告訴原先 invoke 它的 Coroutine 它做完了?

這裡拿上面那段看了眼睛很痛的來說:

suspend fun suspendFunction(text:String) = withContext(Dispatchers.IO){

val result = doSomethingLongTimeProcess(text)
result //result 是個 String
}

它 decompile 後:

public static final Object suspendFunction(@NotNull final String text, @NotNull Continuation $completion) {
return BuildersKt.withContext(
(CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
//...ignore }), $completion);
}

會發現,這個 function return 的不是 String,那是甚麼呢?直接說結論,就是 return COROUTINE_SUSPENDED。

要證明這點其實也很簡單,寫個 java ,invoke 該 suspendFunction 就可以了

Object text = instance.suspendFunction("", new Continuation<String>() {
@NotNull
@Override
public CoroutineContext getContext() {
return Dispatchers.getMain();
}

@Override
public void resumeWith(@NotNull Object o) {

}
});
System.out.println(text);

結果:

COROUTINE_SUSPENDED

Process finished with exit code 0

PS:但這也是這個 function 有包裝了 Coroutine ,若是該 function 沒有包裝 Coroutine 只有標示 suspend ,會直接回傳結果。

那到這邊整體流程的線索足夠,可以解釋那段程式碼了。

fun main() {
GlobalScope.launch {
val text = suspendFunction("text")
println(text) // print after delay
}

}

suspend fun suspendFunction(text:String) = withContext(Dispatchers.IO){

val result = doSomethingLongTimeProcess(text)
result
}

首先,Kotlin compiler 會把 main() 裡面那段 Coroutine 生成一個 Continuation,而 launch block 的部分生成一個有限的狀態機,並包裝進 Continuation 裡那個叫 invokeSuspend(result) 的 method 裏頭,並做初次 resumeWith

Continuation { // GlobalScope.Lanuch()
var label = 0
fun invokeSuspend(result:Any):Any{
when(label){
0->{
val functionResult = suspendFunction("text",this)
lable = 1
if(functionResult == COROUTINE_SUSPENDED){
return functionResult
}
}
1->{
throwOnFailure(result)
break
}
}
val text = result as String
print(text)
}
}

invokeSuspend(result) 會在該 Continuation 的 resumeWith 被 呼叫的時候被呼叫。

Continuation { // GlobalScope.Lanuch()
var label = 0
fun invokeSuspend(result:Any):Any{
when(label){
0->{
val functionResult = suspendFunction("text",this)
lable = 1
if(functionResult == COROUTINE_SUSPENDED){
return functionResult
}
}
1->{
throwOnFailure(result)
break
}
}
val text = result as String
print(text)
}
}

第一次執行 invokeSuspend(result) 的時候,會執行到 suspendFunction(String),並傳入包裝好的 Continuation 。

Continuation { // suspendFunction(text)
fun invokeSuspend(result:Any):Any{
when(label){
0->{
val text = doSomethingLongTimeProcess(context)
return 後執行 continuation.resultWith(text)


}
}
}
}

suspendFunction 自己本身也是一個 Coroutine ,所以它也會包裝成一個 Continuation (但這邊就單純很多,雖然也會生成狀態機,但其實就是就直接跑 doSomethingLongTimeProcess())。

Continuation { // GlobalScope.Lanuch()
var label = 0
fun invokeSuspend(result:Any):Any{
when(label){
0->{
val functionResult = suspendFunction("text",this)
lable = 1
if(functionResult == COROUTINE_SUSPENDED){
return functionResult
}

}
1->{
throwOnFailure(result)
break
}
}
val text = result as String
print(text)
}
}

那因為會進行耗時執行,所以直接回傳 COROUTINE_SUSPENDED,讓原先執行該 Coroutine 的 Thread 會先 return 並執行其他東西,而 suspendFunction 則再另一條 Thread 上面把耗時任務完成。

Continuation { // GlobalScope.Lanuch()
var label = 0
fun invokeSuspend(result:Any):Any{
when(label){
0->{
val functionResult = suspendFunction("text",this)
lable = 1
if(functionResult == COROUTINE_SUSPENDED){
return functionResult
}
}
1->{
throwOnFailure(result)
break
}

}
val text = result as String
print(text)

}
}

等待 suspendFunction 的耗時任務完成後,利用傳入的 Continuation 的 resumeWith 把結果傳入,這個動作同時執行到呼叫它的 Coroutine 的 invokeSuspend(result),並傳入結果,這個動作就能讓 Coroutine 得到 suspendFunction(String) 的結果。

PS:上面那段其實是幫助了解的假碼,實際會遠比看到的複雜

所以實作上,Coroutine 其實是相當反人類的我把我的 callback 給你,等你結束後用那個 callback 告訴我,你把你的 callback 給我,等我結束後我用這個 callback 通知你。

那它的 Thread 是怎麼決定的?

原則上,Coroutine 在使用 Scope 跑起來時,就會決定好要用哪個 Dispatcher ,然後就會建立 Coroutine,一般的情況下,會走到 startCoroutineCancellable,然後 call 到 createCoroutineUnintercepted,那執行上,就是上面講了一票的 resumeWith 和 invokeSuspend。

在追的過程中,有機會會看到這個

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
runSafely(completion) {
createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit))
}

createCoroutineUnintercepted 最後會產出一個 Continuation ,而resumeCancellableWith 其實就是我上面提到的初始化動作, call 這行會去執行產出的狀態機 case 0。

至於 intercepted() ,到底要截斷啥,其實就是把產出來的 Continuation 攔截給指定的 ContinuationInterceptor (這東西包裝在 CoroutineContext 裡面,原則上在指定 Dispatcher 的時候就會建立好了)

public fun intercepted(): Continuation<Any?> =
intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }

這裡可以注意到 interceptContinuation(Continuation) ,可以用他追下去,發現他是 ContinuationInterceptor 的 method ,再追下去可以發現 CoroutineDispatcher 繼承了他:

public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)

那可以發現這動作產生了一個 DispatchedContinuation,看看 DispatchedContinuation ,可以注意到剛才有提到的 resumeCancellableWith

inline fun resumeCancellableWith(result: Result<T>) {
val state = result.toState()
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_CANCELLABLE
dispatcher.dispatch(context, this)
} else {
executeUnconfined(state, MODE_CANCELLABLE) {
if (!resumeCancelled()) {
resumeUndispatchedWith(result)
}
}
}
}

那這邊原則上就會利用 dispatcher 決定需不需要 dispatch 給其他過去了,沒有就直接跑了 resumeUndispatchedWith

@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
inline fun resumeUndispatchedWith(result: Result<T>) {
withCoroutineContext(context, countOrElement) {
continuation.resumeWith(result)
}
}

其實就是直接跑 continuation 的 resumeWith。

那回頭看一下,其實就可以發現是 CoroutineDispatcher 決定要用甚麼 Thread 了。

public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true public abstract fun dispatch(context: CoroutineContext, block: Runnable)
@InternalCoroutinesApi
public open fun dispatchYield(context: CoroutineContext, block: Runnable) = dispatch(context, block)
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)
@InternalCoroutinesApi
public override fun releaseInterceptedContinuation(continuation: Continuation<*>) {
(continuation as DispatchedContinuation<*>).reusableCancellableContinuation?.detachChild()
}
}

那其實知道這東西後,就可以像下去找它的 Child ,就能找到 HandlerDispatcher 了。

那 isDispatchNeeded 就是說是否需要更換 Thread。

而 dispatch 則是更換線程的動作。

那可以看到這兩個 method 在 HandlerDispatcher 的實作:

override fun isDispatchNeeded(context: CoroutineContext): Boolean {
return !invokeImmediately || Looper.myLooper() != handler.looper
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block)
}

靠腰阿, CoroutineContext 根本沒用到阿。

嘛,這主要是因為,Coroutine 是設計給 Kotlin 用的,而並不是專門設計給 Android 用的,所以其實 Android 要用的話,還是需要實作 CoroutineDispatcher 的部分,實際上是兩個體系的東西。那 CoroutineDispatcher 的 dispatch 有提供 CoroutineContext,但不見得 Android 這邊會用到,所以就有這個情況了。

其他諸如 Dispatcher.Default ,他用到了 Executor ,Dispatcher.IO 則是用到了一個叫 WorkQueue 的東西,我覺得最有趣的是 Dispatcher.Main,因為名稱不同,結果用到反射,還蠻硬的,也許未來會有更新我在猜:

所以其實每一個 Dispatcher 都有自己一套又是另一個可以開新篇章的玩意了,目前有提供四種 Dispatcher ,背後其實後還蠻有故事的,這邊就挖坑,然後不填好了。

結語

跟朋友聊,研究這個會花一堆時間,然後其實使用起來才幾行,嘛,說真的我研究這個真的就是研究身體健康的(大概花了 20 小時在看這個吧),其實這種東西看主要是想要了解自己到底用的玩意到底是怎麼去跑的,很多時候技術說開了不困難,但併在一塊跑起來就是讓人霧裡看花阿。

那這邊我這邊打個預防針,這邊的程式碼只是幫助了解的,並不絕對正確,而且因為 Coroutine 目前還在持續更新,所以這邊的東西在未來可能又會不一樣了。

如果有任何問題,或是看到寫錯字,請不要吝嗇對我發問或對我糾正,您的回覆和迴響會是我邊緣人我寫文章最大的動力。

這篇寫完一個周末加幾個晚上蒸發惹,說起來,我幹嘛不先寫 Channel 呢...(抓抓。

參考文章

--

--

Jast Lai
Jastzeonic

A senior who happened to be an Android engineer.