簡介
使用kotlin攜程,難免會使用到攜程的掛起特性奋早,正因為這些特性解決了kotlin等待異步執(zhí)行結(jié)果的回調(diào)地獄兜粘,下面將從源碼的角度來分析攜程的掛起和恢復原理绞绒。
技巧
方法執(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
掛起
特點是:掛起而不阻塞線程
,這里要清楚一點耕腾,掛起的本質(zhì)是切線程
见剩,并且在相應(yīng)的邏輯處理完成之后,再重新切回線程扫俺。掛起使協(xié)程體的操作被return而停止苍苞,等待恢復,它阻塞的是協(xié)程體的操作牵舵,并未阻塞線程。
掛起函數(shù)底層實現(xiàn)
剛開始學習kotlin的同學可能不知道怎么分析kotlin相關(guān)功能代碼倦挂,特別是語法糖相關(guān)的畸颅,其實不管kotlin語法糖再多,它最終要通過編譯
方援,脫糖
生成字節(jié)碼没炒,總是能夠分析的。
下面將通過源碼層面進行攜程掛起
和恢復
的講解犯戏,這里只給出關(guān)鍵的源碼位置送火,如果大家想一步步跟蹤代碼的執(zhí)行邏輯,可以寫一個簡單的apk先匪,里面包含簡單的攜程代碼种吸,利用Android Studio進行編譯和脫糖相關(guān)處理,在用jadx
進程反編譯呀非,查看坚俗。
通過 jdax發(fā)現(xiàn)部分代碼不能反編譯,那么我們可以結(jié)合Android Studio提供的kotlin反編譯工具進行查看岸裙。
-
負責協(xié)程體邏輯的處理(BaseContinuationImpl)
internal abstract class BaseContinuationImpl( // completion:實參是一個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í)行,當invokeSuspend()返回值為COROUTINE_SUSPENDED時剧董,會執(zhí)行return操作幢尚,resumeWith()的執(zhí)行被結(jié)束掉,協(xié)程體的操作也被結(jié)束掉了翅楼,而COROUTINE_SUSPENDED代表協(xié)程發(fā)生掛起侠草。
通過反編譯可以發(fā)現(xiàn)我們編寫的攜程體會被轉(zhuǎn)換成invokeSuspend方法調(diào)用 ,以這個例子進行反編譯(
我用的Android Studio自帶的kotlin工具犁嗅,和通過jadx反編譯出來有所不同边涕,但是掛起和恢復的邏輯都一樣的
)。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ù)實現(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é)合源碼看一下,默認情況下label==0功蜓,i==0,執(zhí)行l(wèi)abel = 1賦值操作园爷,及調(diào)用掛起函數(shù)
delay(...)
,此處判斷delay(...)
方法返回值為coroutine_suspended
時式撼,就會返回coroutine_suspended
童社,也就是當delay(...)內(nèi)存在掛起操作的時候它的返回值就是coroutine_suspended
。假設(shè)
delay(...)
掛起函數(shù)內(nèi)執(zhí)行了掛起操作著隆,delay(...)
方法結(jié)束并返回coroutine_suspended扰楼,resumeWith()方法在收到返回值coroutine_suspended也進行了return操作,resumeWith()和invokeSuspend()方法執(zhí)行都結(jié)束了美浦,println
日志打印并沒有得到執(zhí)行弦赖,協(xié)程掛起并不是阻塞了當前的線程(通過上面第三個例子輸出可以看出
),而是執(zhí)行了return操作浦辨,結(jié)束了協(xié)程體的調(diào)用蹬竖。掛起函數(shù)內(nèi)執(zhí)行掛起操作的時候會返回coroutine_suspended標志,結(jié)束協(xié)程體的運行流酬,使協(xié)程掛起币厕,接下來看下協(xié)程提供的掛起函數(shù)中是如何操作的。
-
攜程恢復
恢復外部協(xié)程時芽腾,通過線程調(diào)度旦装,將協(xié)程在指定線程運行,這樣也就可以在掛起恢復時摊滔,重新切回線程,再次觸發(fā)invokeSuspend()同辣,根據(jù)label狀態(tài)值,執(zhí)行下一個代碼片惭载。結(jié)論
在DispatchedCoroutine中旱函,重寫了afterCompletion()及afterResume(),并且afterCompletion()調(diào)用afterResume()描滔,而afterResume()中首先判斷了協(xié)程是否被掛起棒妨,如已掛起則恢復外部的協(xié)程『ぃ恢復外部協(xié)程時券腔,同樣是通過線程調(diào)度,將協(xié)程在指定線程運行拘泞,這樣也就可以在掛起恢復時纷纫,重新切回線程,再次觸發(fā)invokeSuspend(),根據(jù)label狀態(tài)值陪腌,執(zhí)行下一個代碼片辱魁。本質(zhì)就是:將攜程體代碼分成一個個執(zhí)行代碼塊烟瞧,通過label控制執(zhí)行那一個代碼塊,當執(zhí)行到需要掛起的代碼塊會掛起染簇,然后返回結(jié)束協(xié)調(diào)體執(zhí)行参滴,當掛起部分恢復時,重新在指定線程調(diào)用invokeSuspend方法锻弓,這是label變成下一個要執(zhí)行的代碼塊的值
砾赔。