Kotlin掛起函數原理示例剖析
一、序言
Kotlin掛起函數平時在學習和工作中用的比較多,掌握其原理還是很有必要的。本文將一步一步帶著大傢分析其原理實現。
ps: 文中所用的Kotlin版本是1.7.0。
二、CPS原理
在某個Kotlin函數的前面加個suspend函數,它就成瞭掛起函數(雖然內部不一定會掛起,內部不掛起的稱為偽掛起函數)。
先隨便寫個掛起函數
suspend fun getUserName(): String { delay(1000L) return "雲天明" }
然後通過Android Studio的Tools->Kotlin->Show Kotlin Bytecode->Decompile,現在我們拿到瞭Kotlin字節碼反編譯之後的Java代碼:
public static final Object getUserName(@NotNull Continuation var0) { ... }
可以看到該函數被編譯之後,多瞭一個Continuation參數,其次,返回值變成瞭Object。下面,我們詳細來討論一下這2種變化:函數參數和函數返回值。
CPS參數變化
上面的suspend fun getUserName(): String
函數,如果我在Java中調用的話,會看到Android Studio提示我們
從圖中可以看到,新增瞭一個參數,也就是Continuation,它其實是一個Callback,隻是換瞭個名字而已。
來看下它的定義:
/** * Interface representing a continuation after a suspension point that returns a value of type `T`. */ @SinceKotlin("1.3") public interface Continuation<in T> { /** * 當前continuation所在協程的上下文 */ public val context: CoroutineContext /** * 繼續執行後面的協程代碼,同時把結果回調出去,結果可能是成功或失敗 */ public fun resumeWith(result: Result<T>) }
這個Callback接口會在resumeWith回調結果給外部。
CPS返回值變化
在上面的Continuation接口的定義中,其實還有個小細節,它帶瞭個泛型T。這個泛型T就是我們suspend函數返回值的類型,上面的getUserName返回值是String,編譯之後,這個String就來到瞭Continuation的泛型中。
而getUserName編譯之後的返回值變成瞭Object。為啥是Object?它有什麼用?這個返回值其實是用來標識該函數是否掛起的標志,如果返回值是Intrinsics.COROUTINE_SUSPENDED
,那麼說明該函數被掛起瞭(掛起函數的結果不是通過函數返回值來獲取的,而是通過Continuation,也就是Callback回調得到的結果)。
如果該函數是偽掛起函數(裡面沒有其他掛起函數,但還是會進行CPS轉換),則是直接返回結果。
舉個例子,下面這個就是真正的掛起函數:
suspend fun getUserName(): String { delay(1000L) return "雲天明" }
當執行到delay的時候,就會返回Intrinsics.COROUTINE_SUSPENDED
表示該函數被掛起瞭。
下面這個則是偽掛起函數:
suspend fun getName():String { return "程心" }
這種偽掛起函數不會返回Intrinsics.COROUTINE_SUSPENDED
,而是直接返回結果,它不會被掛起。它看起來就僅僅是一個普通函數,但還是會進行CPS轉換,CPS轉換隻認suspend關鍵字。你如果像上面這樣寫,其實Android Studio也會提示你,說這個suspend關鍵字沒用,叫你把它移除掉。
所以,suspend函數編譯之後的返回值變成瞭Object,因為要兼容偽掛起函數的返回值,而偽掛起函數可能返回任何值,而且還可能為空。
下面我們就來詳細的探索一下掛起函數的底層原理,看看掛起函數反編譯之後是什麼樣子。
三、掛起函數的反編譯
我們先寫個很簡單的suspend函數,然後將其反編譯,然後分析一下。具體的流程是我們用Android Studio寫個掛起函數的demo,然後編譯成apk,然後將apk用jadx反編譯一下,拿到對應class的反編譯Java源碼,這樣弄出來的源碼我感覺比直接通過Android Studio的Tools->Kotlin->Show Kotlin
拿到的源碼稍微好看懂一些。
首先,我創建瞭一個CpsTest.kt文件,然後在裡面寫瞭一個函數:
package com.xfhy.coroutine import kotlinx.coroutines.delay suspend fun getUserName(): String { delay(1000L) return "雲天明" }
就這樣,一個很普通的掛起函數,在內部隻是簡單調用瞭下delay,延遲1000L,再返回結果“雲天明”。雖然這個函數很簡單,但反編譯出來的代碼卻有點多,而且不好看懂,我先把原代碼貼出來,待會兒再放我重新組織過的代碼,作為對比:
public final class CpsTestKt { @Nullable public static final Object getUserName(@NotNull Continuation var0) { Object $continuation; label20: { if (var0 instanceof <undefinedtype>) { $continuation = (<undefinedtype>)var0; if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) { ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE; break label20; } } $continuation = new ContinuationImpl(var0) { // $FF: synthetic field Object result; int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return CpsTestKt.getUserName(this); } }; } Object $result = ((<undefinedtype>)$continuation).result; Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(((<undefinedtype>)$continuation).label) { case 0: ResultKt.throwOnFailure($result); ((<undefinedtype>)$continuation).label = 1; if (DelayKt.delay(1000L, (Continuation)$continuation) == var3) { return var3; } break; case 1: ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } return "雲天明"; } }
這反編譯之後的東西不太好看懂,我重新組織瞭一下:
public final class CpsTestKt { public static final Object getUserName(Continuation<? super java.lang.String> continuation) { //這個TestContinuation實質上是一個匿名內部類,這裡給它取個名字而已 final class TestContinuation extends ContinuationImpl { //協程狀態機當前的狀態 int label; //保存invokeSuspend回調時吐出來的返回結果 Object result; TestContinuation(Continuation continuation) { super(continuation); } //invokeSuspend比較重要,它是狀態機的入口,會將執行流程交給getUserName再次調用 //協程的本質,就是CPS+狀態機 public final Object invokeSuspend(Object obj) { //callback回調時會把結果帶出來 this.result = obj; this.label |= Integer.MIN_VALUE; //開啟協程狀態機 return CpsTestKt.getUserName(this); } } TestContinuation testContinuation; label20: { //不是第一次進入,則走這裡,把continuation轉成TestContinuation,TestContinuation隻會生成一個實例,不會每次都生成。 if (continuation instanceof TestContinuation) { testContinuation = (TestContinuation) continuation; if ((testContinuation.label & Integer.MIN_VALUE) != 0) { testContinuation.label -= Integer.MIN_VALUE; break label20; } } //如果是第一次進入getUserName,則TestContinuation還沒被創建,會走到這裡,此時先去創建一個TestContinuation testContinuation = new TestContinuation(continuation); } //將之前執行的結果取出來 Object $result = testContinuation.result; //掛起的標志,如果需要掛起的話,就返回這個flag Object flag = IntrinsicsKt.getCOROUTINE_SUSPENDED(); //狀態機 switch (testContinuation.label) { case 0: // 檢測異常 ResultKt.throwOnFailure($result); //將label的狀態改成1,方便待會兒執行delay後面的代碼 testContinuation.label = 1; //0. 調用DelayKt.delay函數 //1. 將testContinuation傳瞭進去 //2. DelayKt.delay是一個掛起函數,正常情況下,它會立馬返回一個值:IntrinsicsKt.COROUTINE_SUSPENDED(也就是這裡的flag),表示該函數已被掛起,這裡就直接return瞭,該函數被掛起 //3. 恢復執行:在DelayKt.delay內部,到瞭指定的時間後就會調用testContinuation這個Callback的invokeSuspend //4. invokeSuspend中又將執行getUserName函數,同時將之前創建好的testContinuation傳入其中,開始執行後面的邏輯(label為1的邏輯),該函數繼續往後面執行(也就是恢復執行) if (DelayKt.delay(1000L, testContinuation) == flag) { return flag; } break; case 1: // 檢測異常 ResultKt.throwOnFailure($result); //label 1這裡沒有return,而是會走到下面的return "雲天明"語句 break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } return "雲天明"; } }
在getUserName函數中,會多出一個ContinuationImpl的子類,它是一個匿名內部類(為瞭方便,給它取瞭個名字TestContinuation),也是整個協程掛起函數的核心。在這個TestContinuation中有2個變量
- label: 協程狀態機當前的狀態
- result: 保存invokeSuspend回調時吐出來的返回結果
invokeSuspend是一個抽象方法,當協程從掛起狀態想要恢復時,就得調用這個invokeSuspend,然後繼續走狀態機邏輯,繼續執行後面的代碼。具體是怎麼調用這個invokeSuspend的,後面有機會再細說。暫時我們隻要知道,這裡是恢復的入口就行。invokeSuspend內部會把結果(這個結果可能是正常的結果,也可能是Exception)取出來,開啟協程狀態機。
分析完TestContinuation,再來看一下第一次進入getUserName是怎麼走的。
- 首先,第一次進入時,continuation肯定不是TestContinuation,因為此時還沒有new過TestContinuation實例,所以會走到創建TestContinuation的邏輯,並且會把continuation包進去。
- 然後剛創建完的testContinuation的label未賦其他值,那就是初始值0瞭。那麼switch狀態機那裡,就走case 0,先把label改成1,因為馬上就要掛起瞭,待會兒恢復時需要執行下一個狀態的代碼。
- 調用Kotlin的庫函數delay,它是一個掛起函數,將testContinuation傳入其中,方便它進行invokeSuspend回調。調用掛起函數,那麼它可能會返回
COROUTINE_SUSPENDED
,表示它已經被掛起瞭,如果是掛起瞭那麼getUserName就走完瞭,到時會從invokeSuspend恢復。在還沒有恢復的時候,這個協程所在的線程可以去做其他事情。
恢復的時候,又開始從頭走getUserName,此時的continuation已經是TestContinuation,不會重新創建。它的label之前已經被改成1瞭的,所以switch狀態機那裡,會走到case 1,先檢測一下有沒有異常,沒有異常就返回真正的返回值瞭“雲天明”。
分析到這裡也就完瞭,上面就是一個非常簡單的掛起函數的反編譯分析的整個過程。下面我們簡單分析一下偽掛起函數會帶來什麼效果。
四、偽掛起函數
在之前的CpsTest.kt裡面簡單改一下
suspend fun fakeSuspendFun() = "維德" suspend fun getUserName(): String { println(fakeSuspendFun()) return "雲天明" }
像fakeSuspendFun這種就是偽掛起函數,平時不建議像fakeSuspendFun這麼寫,即使寫瞭,Android Studio也會提示你,這suspend關鍵字沒用,內部沒有掛起。它內部沒有掛起的邏輯,但是它有suspend關鍵字,那麼Kotlin編譯器依然會給它做CPS轉換。
public final class CpsTestKt { @Nullable public static final Object fakeSuspendFun(@NotNull Continuation<? super java.lang.String> $completion) { return "維德"; } @Nullable public static final Object getUserName(@NotNull Continuation<? super java.lang.String> continuation) { final class TestContinuation extends ContinuationImpl { int label; Object result; TestContinuation(Continuation continuation) { super(continuation); } public final Object invokeSuspend(Object obj) { this.result = obj; this.label |= Integer.MIN_VALUE; return CpsTestKt.getUserName(this); } } TestContinuation testContinuation; label20: { if (continuation instanceof TestContinuation) { testContinuation = (TestContinuation) continuation; if ((testContinuation.label & Integer.MIN_VALUE) != 0) { testContinuation.label -= Integer.MIN_VALUE; break label20; } } testContinuation = new TestContinuation(continuation); } Object $result = testContinuation.result; Object flag = IntrinsicsKt.getCOROUTINE_SUSPENDED(); //變化在這裡,這個變量用來存儲fakeSuspendFun的返回值 Object var10000; switch(testContinuation.label) { case 0: ResultKt.throwOnFailure($result); testContinuation.label = 1; var10000 = fakeSuspendFun((Continuation)$continuation); if (var10000 == flag) { //如果是掛起,那麼直接返回COROUTINE_SUSPENDED return flag; } //顯然,這裡是不會掛起的,會走這裡的break break; case 1: ResultKt.throwOnFailure($result); var10000 = $result; break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } //走這裡 Object var1 = var10000; System.out.println(var1); return "雲天明"; } }
在調用偽掛起函數時,不會掛起,它不會返回COROUTINE_SUSPENDED
,而是繼續往下走。
五、多個掛起函數前後關聯
平時在工作中,可能經常會有多個掛起函數前後是關聯的,後面一個掛起函數需要前面一個掛起函數的結果來幹點事情,比上面隻有一個getUserName掛起函數稍微復雜些,我們來分析一下。
比如我們拿到一個需求,展示我的朋友圈,假設獲取流程如下:獲取用戶id->根據用戶id獲取該用戶的好友列表->獲取好友列表每個人的朋友圈。下面是非常簡單的實現:
//需求: 獲取用戶id->根據用戶id獲取該用戶的好友列表->獲取好友列表每個人的朋友圈 suspend fun showMoments() { println("start") val userId = getUserId() println(userId) val friendList = getFriendList(userId) println(friendList) val feedList = getFeedList(userId, friendList) println(feedList) } suspend fun getUserId(): String { delay(1000L) return "1sa13124daadar2" } suspend fun getFriendList(userId: String): String { println("正在獲取${userId}的朋友列表") delay(1000L) return "雲天明, 維德" } suspend fun getFeedList(userId: String, list: String): String { println("獲取${userId}的朋友圈($list)") delay(1000L) return "雲天明: 酒好喝嗎?煙好抽嗎?即使是可口可樂,第一次嘗也不好喝,讓人上癮的東西都是這樣;\n維德: 前進!前進!!不擇手段地前進!!!" }
它的執行結果如下:
start
1sa13124daadar2
正在獲取1sa13124daadar2的朋友列表
雲天明, 維德
獲取1sa13124daadar2的朋友圈(雲天明, 維德)
雲天明: 酒好喝嗎?煙好抽嗎?即使是可口可樂,第一次嘗也不好喝,讓人上癮的東西都是這樣;
維德: 前進!前進!!不擇手段地前進!!!
end
這段代碼要稍微復雜一些,這些掛起函數前後關聯,前面獲取到的數據後面的掛起函數需要使用到。相應的,它們反編譯之後也要復雜一些。但是沒關系,我已經把晦澀難懂的代碼重新組裝瞭一下,方便大傢閱讀。同時,在下面的代碼中,每一步在走哪個分支,都有詳細的註釋分析,幫助大傢理解。
public final class TestSuspendKt { @Nullable public static final Object showMoments(@NotNull Continuation<? super Unit> continuation) { ShowMomentsContinuation showMomentsContinuation; label37: { if (continuation instanceof ShowMomentsContinuation) { //非第一次進showMoments,則走這裡,continuation已經是ShowMomentsContinuation瞭 showMomentsContinuation = (ShowMomentsContinuation)continuation; if ((showMomentsContinuation.label & Integer.MIN_VALUE) != 0) { showMomentsContinuation.label -= Integer.MIN_VALUE; break label37; } } //第一次,走這裡,初始化ShowMomentsContinuation,將傳入的continuation包起來 showMomentsContinuation = new ShowMomentsContinuation(continuation); final class ShowMomentsContinuation extends ContinuationImpl { int label; Object result; //存放臨時數據 Object tempData; ShowMomentsContinuation(Continuation continuation) { super(continuation); } public final Object invokeSuspend(Object obj) { this.result = obj; this.label |= Integer.MIN_VALUE; return CpsTestKt.getUserName(this); } } } //存放每個函數的返回結果,臨時放一下 Object computeResult; label31: { String userId; Object flag; label30: { //從continuation中把result取出來 Object $result = showMomentsContinuation.result; flag = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(showMomentsContinuation.label) { case 0: //第一次,走這裡,檢測異常 ResultKt.throwOnFailure($result); System.out.println("start"); //將label改成1 showMomentsContinuation.label = 1; //執行getUserId函數,computeResult用來接收返回值 computeResult = getUserId((Continuation)showMomentsContinuation); //getUserId是掛起函數,不出意外的話,computeResult的值會是COROUTINE_SUSPENDED,這裡就直接return瞭 //showMoments函數這一次執行,就算完成瞭。 //恢復執行時,會走ShowMomentsContinuation 的invokeSuspend,走下面label等於1的邏輯 if (computeResult == flag) { return flag; } break; case 1: //第二次執行showMoments時,label已經等於1瞭,走這裡. ResultKt.throwOnFailure($result); computeResult = $result; break; case 2: //第三次執行showMoments時,label已經等於2瞭,走這裡. //先將之前暫存的userId取出來,馬上需要用到 userId = (String)showMomentsContinuation.tempData; ResultKt.throwOnFailure($result); computeResult = $result; break label30; case 3: //第四次執行showMoments時,label已經等於3瞭,走這裡. ResultKt.throwOnFailure($result); computeResult = $result; break label31; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } //第二次執行showMoments時,label=1,會走到這裡來,將getUserId函數回調回來的userId保存起來,並輸出 userId = (String)computeResult; System.out.println(userId); //將userId放continuation裡面暫存起來 showMomentsContinuation.tempData = userId; //又要執行掛起函數瞭,這裡將label改成2 showMomentsContinuation.label = 2; //開始調用getFriendList computeResult = getFriendList(userId, (Continuation)showMomentsContinuation); //getFriendList是掛起函數,不出意外的話,computeResult的值會是COROUTINE_SUSPENDED,這裡就直接return瞭 //showMoments函數這一次執行,就算完成瞭。 //恢復執行時,會走ShowMomentsContinuation 的invokeSuspend,走上面label等於2的邏輯 if (computeResult == flag) { return flag; } } //第三次執行showMoments時,label=2,會走到這裡來,將getFriendList函數回調回來的friendList輸出 String friendList = (String)computeResult; System.out.println(friendList); showMomentsContinuation.tempData = null; //又要執行掛起函數瞭,這裡將label改成3 showMomentsContinuation.label = 3; //開始調用getFeedList computeResult = getFeedList(userId, friendList, (Continuation)showMomentsContinuation); //getFeedList是掛起函數,不出意外的話,computeResult的值會是COROUTINE_SUSPENDED,這裡就直接return瞭 //showMoments函數這一次執行,就算完成瞭。 //恢復執行時,會走ShowMomentsContinuation 的invokeSuspend,走上面label等於3的邏輯 if (computeResult == flag) { return flag; } } //第四次執行showMoments時,label=3,會走到這裡來,將getFeedList函數回調回來的feedList輸出 String feedList = (String)computeResult; System.out.println(feedList); System.out.println("end"); //showMoments函數這一次執行,就算完成瞭。 //沒有剩下的掛起函數需要執行瞭 return Unit.INSTANCE; } //因為getUserId、getFriendList、getFeedList中的匿名內部類邏輯與showMoments中的一模一樣,故沒有將其重新組織語言 @Nullable public static final Object getUserId(@NotNull Continuation var0) { Object $continuation; label20: { //這裡的<undefinedtype>就是在getUserId函數裡生成的new ContinuationImpl匿名內部類 if (var0 instanceof <undefinedtype>) { $continuation = (<undefinedtype>)var0; if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) { ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE; break label20; } } $continuation = new ContinuationImpl(var0) { // $FF: synthetic field Object result; int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return TestSuspendKt.getUserId(this); } }; } Object $result = ((<undefinedtype>)$continuation).result; Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(((<undefinedtype>)$continuation).label) { case 0: //第一次執行getUserId時,走這裡 ResultKt.throwOnFailure($result); //馬上要開始執行掛起函數瞭,label先改一下 ((<undefinedtype>)$continuation).label = 1; //執行delay,正常情況下,會返回COROUTINE_SUSPENDED,於是getUserId這一次就執行完瞭,return瞭 //恢復時會回調上面的匿名內部類$continuation中的invokeSuspend if (DelayKt.delay(1000L, (Continuation)$continuation) == var3) { return var3; } break; case 1: //第二次執行getUserId時,也就是delay執行完回來,走這裡 ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } //拿到數據,getUserId就算真正的執行完瞭,接著會去執行showMoments函數中的ShowMomentsContinuation#invokeSuspend,也就是恢復showMoments,繼續執行showMoments中getUserId後面的邏輯 return "1sa13124daadar2"; } @Nullable public static final Object getFriendList(@NotNull String userId, @NotNull Continuation var1) { Object $continuation; label20: { if (var1 instanceof <undefinedtype>) { $continuation = (<undefinedtype>)var1; if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) { ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE; break label20; } } $continuation = new ContinuationImpl(var1) { // $FF: synthetic field Object result; int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return TestSuspendKt.getFriendList((String)null, this); } }; } Object $result = ((<undefinedtype>)$continuation).result; Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(((<undefinedtype>)$continuation).label) { case 0: ResultKt.throwOnFailure($result); String var2 = "正在獲取" + userId + "的朋友列表"; System.out.println(var2); ((<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"); } return "雲天明, 維德"; } @Nullable public static final Object getFeedList(@NotNull String userId, @NotNull String list, @NotNull Continuation var2) { Object $continuation; label20: { if (var2 instanceof <undefinedtype>) { $continuation = (<undefinedtype>)var2; if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) { ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE; break label20; } } $continuation = new ContinuationImpl(var2) { // $FF: synthetic field Object result; int label; @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return TestSuspendKt.getFeedList((String)null, (String)null, this); } }; } Object $result = ((<undefinedtype>)$continuation).result; Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(((<undefinedtype>)$continuation).label) { case 0: ResultKt.throwOnFailure($result); String var3 = "獲取" + userId + "的朋友圈(" + list + ')'; System.out.println(var3); ((<undefinedtype>)$continuation).label = 1; if (DelayKt.delay(1000L, (Continuation)$continuation) == var6) { return var6; } break; case 1: ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } return "雲天明: 酒好喝嗎?煙好抽嗎?即使是可口可樂,第一次嘗也不好喝,讓人上癮的東西都是這樣;\n維德: 前進!前進!!不擇手段地前進!!!"; } }
觀察源碼,發現一些東西:
- 每個掛起函數都有一個匿名內部類,繼承ContinuationImpl,在invokeSuspend中開啟狀態機
- 每個掛起函數都經過瞭CPS轉換
- 在掛起之後,當前執行協程的這個線程其實是空閑的,沒有代碼交給它執行。在invokeSuspend恢復之後,才繼續執行
- 每個掛起函數,都有一個狀態機
- 掛起函數中的邏輯被分塊執行(也就是狀態機那塊的邏輯),分塊的數量=掛起函數數量+1
基本上來說,掛起函數的實現原理就是上面這些瞭。
六、在Java中調用suspend函數
既然Kotlin是兼容Java的,那麼如果我想在Java裡面調用Kotlin的suspend函數按道理也是可以的。那具體如何調用呢?
就拿上面的案例舉例,假設我想在Activity中點擊某個按鈕時調用showMoments這個suspend函數,該怎麼搞?大傢先思考一下,稍後給出答案。
//將上面的案例加瞭個返回值 suspend fun showMoments(): String { println("start") val userId = getUserId() println(userId) val friendList = getFriendList(userId) println(friendList) val feedList = getFeedList(userId, friendList) println(feedList) println("end") return feedList }
因為showMoments函數有suspend關鍵字,那麼最終會經過CPS轉換,有一個Continuation參數。在Java中調用showMoments時,肯定需要把Continuation傳進去才行。Continuation是一個接口,需要傳個實現類過去,把getContext和resumeWith實現起。
TestSuspendKt.showMoments(new Continuation<String>() { @NonNull @Override public CoroutineContext getContext() { return (CoroutineContext) Dispatchers.getIO(); } @Override public void resumeWith(@NonNull Object result) { //這裡的result就是showMoments的返回值 Log.d("xfhy666", "" + result); } });
Java中調用掛起函數,看起來就像是調用瞭一個方法,這個方法需要傳一個callback過去,這個方法的返回值是通過回調給出來的,並且可以自定義該方法運行在哪個線程中。
七、總結
好瞭,今天的Kotlin掛起函數就分析到這裡,基本上謎團已全部解開(除瞭invokeSuspend是在什麼時候回調的,後面有機會再和大傢分享)。
Kotlin的掛起函數,本質上就是:CPS+狀態機。
- CPS:掛起函數比普通函數多瞭suspend關鍵字,Kotlin編譯器會對其特殊處理。將該函數轉換成一個帶有Callback的函數,Callback就是Continuation接口,它的泛型就是原來函數的返回值類型。轉換之後的返回值類型是
Any?
,因為加瞭suspend關鍵字的不一定會被掛起,掛起的話返回Intrinsics.COROUTINE_SUSPENDED
,偽掛起函數(裡面沒有其他掛起函數,但還是會進行CPS轉換)則是直接返回結果,這個結果可以是任何類型,所以返回值隻能是Any?
。 - 狀態機:當掛起函數經過編譯之後,會變成switch和label組成的狀態機結構。label代表瞭當前狀態機的具體狀態,每改變一次,就代表掛起函數被調用一次。在裡面會創建一個Callback接口,當掛起之後,掛起函數的結果返回是通過Callback回調回來的,回調回來之後,因為之前修改過label,根據該label來判斷該繼續往下走瞭,執行後面的邏輯。上面的Callback就是Continuation,我覺得它在這裡的意思可以翻譯成繼續執行剩餘的代碼。
以上就是Kotlin掛起函數原理示例剖析的詳細內容,更多關於Kotlin掛起函數的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Kotlin協程啟動createCoroutine及創建startCoroutine原理
- Kotlin協程launch啟動流程原理詳解
- Kotlin協程launch原理詳解
- Kotlin協程到底是如何切換線程的
- Kotlin Job啟動流程源碼層深入分析