簡介
使用kotlin攜程,難免會使用到攜程的掛起特性,正因為這些特性解決了kotlin等待異步執(zhí)行結(jié)果的回調(diào)地獄,下面將從源碼的角度來分析攜程的掛起和恢復(fù)原理。
技巧
方法執(zhí)行可以通過打印線程堆棧來看
public static void printStackTrace(String msg) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
println(Thread.currentThread() + ", message: [" + msg + "]");
if (stackTrace == null || stackTrace.length < 2) {
println("empty stack");
return;
}
for (int i = 2; i < stackTrace.length; i++) {
println("\tat " + stackTrace[i]);
}
}
private static void println(Object object) {
System.out.println(object);
}
例子先行
- 公用代碼
fun treadName(): String = Thread.currentThread().name suspend fun doSuspendOne(): Int { delay(1000L) // 假設(shè)我們在這里做了一些有用的事 println("${treadName()}======doSuspendOne") return 13 } suspend fun doSuspendTwo(): Int { delay(500L) // 假設(shè)我們在這里也做了一些有用的事 println("${treadName()}======doSuspendTwo") return 29 }
- 第一個例子
輸出如下:fun main() { runBlocking { doSuspendOne() doSuspendTwo() } println("${treadName()}======main") }
main======doSuspendOne main======doSuspendTwo main======main Process finished with exit code 0
- 第二個例子
輸出如下:suspend fun main() { doSuspendOne() doSuspendTwo() println("${treadName()}======main") }
kotlinx.coroutines.DefaultExecutor======doSuspendOne kotlinx.coroutines.DefaultExecutor======doSuspendTwo kotlinx.coroutines.DefaultExecutor======main Process finished with exit code 0
- 第三個例子
輸出如下:suspend fun main() { GlobalScope.launch { println("${treadName()}======launch1111") delay(300) println("${treadName()}======launch") } runBlocking { println("${treadName()}======runBlocking") doSuspendOne() doSuspendTwo() } println("${treadName()}======main") }
DefaultDispatcher-worker-1======launch1111 main======runBlocking DefaultDispatcher-worker-1======launch main======doSuspendOne main======doSuspendTwo main======main Process finished with exit code 0
- 第四個例子
輸出如下suspend fun main() { GlobalScope.launch { println("${treadName()}======launch1111") delay(300) println("${treadName()}======launch") } println("${treadName()}======main") Thread.sleep(3000) }
main======main DefaultDispatcher-worker-1======launch1111 DefaultDispatcher-worker-1======launch Process finished with exit code 0
掛起
特點(diǎn)是:掛起而不阻塞線程
,這里要清楚一點(diǎn),掛起的本質(zhì)是切線程
,并且在相應(yīng)的邏輯處理完成之后,再重新切回線程。掛起使協(xié)程體的操作被return而停止,等待恢復(fù),它阻塞的是協(xié)程體的操作,并未阻塞線程。
掛起函數(shù)底層實(shí)現(xiàn)
剛開始學(xué)習(xí)kotlin的同學(xué)可能不知道怎么分析kotlin相關(guān)功能代碼,特別是語法糖相關(guān)的,其實(shí)不管kotlin語法糖再多,它最終要通過編譯
,脫糖
生成字節(jié)碼,總是能夠分析的。
下面將通過源碼層面進(jìn)行攜程掛起
和恢復(fù)
的講解,這里只給出關(guān)鍵的源碼位置,如果大家想一步步跟蹤代碼的執(zhí)行邏輯,可以寫一個簡單的apk,里面包含簡單的攜程代碼,利用Android Studio進(jìn)行編譯和脫糖相關(guān)處理,在用jadx
進(jìn)程反編譯,查看。
通過 jdax發(fā)現(xiàn)部分代碼不能反編譯,那么我們可以結(jié)合Android Studio提供的kotlin反編譯工具進(jìn)行查看。
-
負(fù)責(zé)協(xié)程體邏輯的處理(BaseContinuationImpl)
internal abstract class BaseContinuationImpl( // completion:實(shí)參是一個AbstractCoroutine public val completion: Continuation<Any?>? ) : Continuation<Any?>, CoroutineStackFrame, Serializable { public final override fun resumeWith(result: Result<Any?>) { ... try { // 調(diào)用invokeSuspend方法,協(xié)程體真正開始執(zhí)行 val outcome = invokeSuspend(param) // invokeSuspend方法返回值為COROUTINE_SUSPENDED,resumeWith方法被return,結(jié)束執(zhí)行,說明執(zhí)行了掛起操作 if (outcome === COROUTINE_SUSPENDED) return // 協(xié)程體執(zhí)行成功的結(jié)果 Result.success(outcome) } catch (exception: Throwable) { // 協(xié)程體出現(xiàn)異常的結(jié)果 Result.failure(exception) } releaseIntercepted() if (completion is BaseContinuationImpl) { ... } else { completion.resumeWith(outcome) return ...
invokeSuspend()的執(zhí)行就是協(xié)程體的執(zhí)行,當(dāng)invokeSuspend()返回值為COROUTINE_SUSPENDED時,會執(zhí)行return操作,resumeWith()的執(zhí)行被結(jié)束掉,協(xié)程體的操作也被結(jié)束掉了,而COROUTINE_SUSPENDED代表協(xié)程發(fā)生掛起。
通過反編譯可以發(fā)現(xiàn)我們編寫的攜程體會被轉(zhuǎn)換成invokeSuspend方法調(diào)用 ,以這個例子進(jìn)行反編譯(
我用的Android Studio自帶的kotlin工具,和通過jadx反編譯出來有所不同,但是掛起和恢復(fù)的邏輯都一樣的
)。suspend fun doSuspendOne(): Int { delay(1000) // 假設(shè)我們在這里做了一些有用的事 println("${treadName()}======doSuspendOne") return 13 } fun main(): Unit = runBlocking{ doSuspendOne() }
看一下invokeSuspend方法:
public final Object invokeSuspend(@NotNull Object $result) { ... switch(this.label) { case 0: ... this.label = 1; if (TestKt.doSuspendOne(this) == var2) { return var2; } break; ... } ... }
doSuspendOne函數(shù)實(shí)現(xiàn):
public static final Object doSuspendOne(@NotNull Continuation var0) { Object $continuation; ... Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(((<undefinedtype>)$continuation).label) { case 0: ResultKt.throwOnFailure($result); ((<undefinedtype>)$continuation).label = 1; if (DelayKt.delay(1000L, (Continuation)$continuation) == var5) { return var5; } break; case 1: ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } String var1 = KotlinCoroutinesTestKt.treadName() + "======doSuspendOne"; boolean var2 = false; System.out.println(var1); return Boxing.boxInt(13); }
結(jié)合源碼看一下,默認(rèn)情況下label==0,i==0,執(zhí)行l(wèi)abel = 1賦值操作,及調(diào)用掛起函數(shù)
delay(...)
,此處判斷delay(...)
方法返回值為coroutine_suspended
時,就會返回coroutine_suspended
,也就是當(dāng)delay(...)內(nèi)存在掛起操作的時候它的返回值就是coroutine_suspended
。假設(shè)
delay(...)
掛起函數(shù)內(nèi)執(zhí)行了掛起操作,delay(...)
方法結(jié)束并返回coroutine_suspended,resumeWith()方法在收到返回值coroutine_suspended也進(jìn)行了return操作,resumeWith()和invokeSuspend()方法執(zhí)行都結(jié)束了,println
日志打印并沒有得到執(zhí)行,協(xié)程掛起并不是阻塞了當(dāng)前的線程(通過上面第三個例子輸出可以看出
),而是執(zhí)行了return操作,結(jié)束了協(xié)程體的調(diào)用。掛起函數(shù)內(nèi)執(zhí)行掛起操作的時候會返回coroutine_suspended標(biāo)志,結(jié)束協(xié)程體的運(yùn)行,使協(xié)程掛起,接下來看下協(xié)程提供的掛起函數(shù)中是如何操作的。
-
攜程恢復(fù)
恢復(fù)外部協(xié)程時,通過線程調(diào)度,將協(xié)程在指定線程運(yùn)行,這樣也就可以在掛起恢復(fù)時,重新切回線程,再次觸發(fā)invokeSuspend(),根據(jù)label狀態(tài)值,執(zhí)行下一個代碼片。結(jié)論
在DispatchedCoroutine中,重寫了afterCompletion()及afterResume(),并且afterCompletion()調(diào)用afterResume(),而afterResume()中首先判斷了協(xié)程是否被掛起,如已掛起則恢復(fù)外部的協(xié)程。恢復(fù)外部協(xié)程時,同樣是通過線程調(diào)度,將協(xié)程在指定線程運(yùn)行,這樣也就可以在掛起恢復(fù)時,重新切回線程,再次觸發(fā)invokeSuspend(),根據(jù)label狀態(tài)值,執(zhí)行下一個代碼片。本質(zhì)就是:將攜程體代碼分成一個個執(zhí)行代碼塊,通過label控制執(zhí)行那一個代碼塊,當(dāng)執(zhí)行到需要掛起的代碼塊會掛起,然后返回結(jié)束協(xié)調(diào)體執(zhí)行,當(dāng)掛起部分恢復(fù)時,重新在指定線程調(diào)用invokeSuspend方法,這是label變成下一個要執(zhí)行的代碼塊的值
。