Kotlin?掛起函數(shù)CPS轉換原理解析
正文
普通函數(shù)加上suspend
之后就成為了一個掛起函數(shù),Kotlin編譯器會將這個掛起函數(shù)轉換成了帶有參數(shù)Continuation<T>
的一個普通函數(shù),Continuation
是一個接口,它跟Java中的Callback
有著一樣的功能,這個轉換過程被稱為CPS轉換。
1.什么是CPS轉換
掛起函數(shù)中的CPS轉換就是把掛起函數(shù)轉換成一個帶有Callback
的函數(shù),這里的 Callback
就是 Continuation
接口。在這個過程中會發(fā)生函數(shù)參數(shù)的變化和函數(shù)返回值的變化。
suspend fun getAreaCode(): String { delay(1000L) return "100011" } //函數(shù)參數(shù)的變化 suspend ()變成了(Continuation) //函數(shù)返回值的變化 -> String變成了 ->Any? //變化后的代碼如下 private fun getProvinceCode(c: Continuation<String>): Any? { return "100000" }
2.CPS的過程是怎么讓參數(shù)改變的
這個問題的答案其實在掛起函數(shù)哪里提到過,Kotlin代碼可以運行主要是Kotlin編譯器將代碼轉換成了Java字節(jié)碼,然后交給Java虛擬機執(zhí)行,那么轉換成Java后的掛起函數(shù)就是一個帶有Callback
回調(diào)的普通函數(shù),對應Kotlin的話就是Continuation
函數(shù),那么這是參數(shù)的改變,代碼的轉換就是:
private suspend fun getProvinceCode(): String { delay(1000L) return "100000" } /** * Kotlin轉換的Java代碼 */ private static final Object getProvinceCode(Continuation $completion) { return "100000"; } private fun getProvinceCode(c: Continuation<String>): Any? { return "100000" }
這里就可以解答一個疑問:為什么普通函數(shù)不可以調(diào)用掛起函數(shù)了? 這是因為掛起函數(shù)被Kotlin編譯器便后默認是需要傳入一個Continuation
參數(shù)的,而普通函數(shù)沒有這個類型的參數(shù)。
3.CPS的過程是怎么讓返回值改變的
原本的代碼是返回了一個String類型的值,但是通過CPS轉換后String變成了Any?,如果說String是Any?的子類這樣也行的通,但是String為什么沒了呢,以及為什么會多了一個Any?
首先解釋這個String為什么沒有了,其實String不是沒有了,而是換了個地方
// 換到了這里 private fun getProvinceCode(c: Continuation<String>): Any? { return "100000" }
CPS轉換它必定是一個等價交換, 否則編譯后的程序就失去了原本的作用,也就是說這個String它會以另一種形式存在。
現(xiàn)在解釋第二個問題,為什么會多了一個Any?
掛起函數(shù)經(jīng)過 CPS 轉換后,它的返回值有一個重要作用:標志該掛起函數(shù)有沒有被掛起。 掛起函數(shù)也有可能不會被掛起,上面的掛起函數(shù)中都添加了delay(1000L)
,而delay(1000L)
是一個掛起函數(shù)這個是已經(jīng)知道的,那么如果不加它會怎么樣呢
上面的函數(shù)刪除了delay(1000L)
只有suspend
成了灰色并且提示信息:suspend是多余的, 用兩段代碼做個對比
//有效的掛起函數(shù) private suspend fun suspendFun(): String { delay(1000L) return "100000" } //無效的掛起函數(shù) private suspend fun noSuspendFun(): String { return "100000" }
反編譯后的Java代碼
//函數(shù)調(diào)用 @Nullable public static final Object main(@NotNull Continuation $completion) { Object var10000 = suspendFun($completion); return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE; } // $FF: synthetic method public static void main(String[] var0) { RunSuspendKt.runSuspend(new SuspendDemoKt$$$main(var0)); } //有效的掛起函數(shù) private static final Object suspendFun(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 SuspendDemoKt.suspendFun(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 "100000"; } //無效的掛起函數(shù) private static final Object noSuspendFun(Continuation $completion) { return "100000"; }
通過代碼可以很清楚的看到suspendFun
和noSuspendFun
兩個函數(shù)的區(qū)別,返回值可能是IntrinsicsKt.getCOROUTINE_SUSPENDED()也有可能是var10000 也可能是Unit.INSTANCE,也有可能是一個null,因此為了滿足所有可能性使用Any?
是最合適的
為什么說Any?
是最合適的?
Kotlin中的Any類似于Java中的Object,Any是不可為空的,Any?是可以為空的,Any?包含Any的同時還包含了可空的類型,也就是說后者的包容性比前者更廣,所以說前者就是后者的子類,同樣的String和String?、Unit和Unit?也是一樣的關系,用圖表示就是這樣
4.掛起函數(shù)的反編譯
這里直接將上面suspendFun
函數(shù)反編譯后的代碼拿來分析
private static final Object suspendFun(Continuation var0) { Object $continuation; label20: { //undefinedtype就是Continuation //不是第一次進入走這里,保證只生成了一個實例 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) { //協(xié)程返回結果 Object result; //表示協(xié)程狀態(tài)機當前的狀態(tài) int label; //invokeSuspend 是協(xié)程的關鍵 //它最終會調(diào)用 suspendFun(this) 開啟協(xié)程狀態(tài)機 //狀態(tài)機相關代碼就是后面的 switch 語句 //協(xié)程的本質(zhì),可以說就是 CPS + 狀態(tài)機 @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return SuspendDemoKt.suspendFun(this); } }; } //取出執(zhí)行的結果 Object $result = ((<undefinedtype>)$continuation).result; //返回是否被掛起的狀態(tài) Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(((<undefinedtype>)$continuation).label) { case 0: //異常判斷 ResultKt.throwOnFailure($result); //這里將label的狀態(tài)改成1,進入下一行delay(1000L)代碼 ((<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 "100000"; }
這里先對幾個變量、函數(shù)進行說明:
- undefinedtype根據(jù)上下問的代碼可以輕松的推斷出來就是Continuation;
- label 是用來代表協(xié)程狀態(tài)機當中狀態(tài)的;
- result 是用來存儲當前掛起函數(shù)執(zhí)行結果的;
- invokeSuspend 這個函數(shù),是整個狀態(tài)機的入口,它會將執(zhí)行流程轉交給 suspendFun() 進行再次調(diào)用。
反編譯的代碼讀起來比較費勁,因為原本提供的掛起函數(shù)代碼的例子比較簡單所以慢慢分析的話還是比較好理解的。
這里首先分析第一段代碼的作用,根據(jù)上面的注釋我將undefinedtype
修改為Continueation
label20: { //undefinedtype就是Continuation //不是第一次進入走這里,保證只生成了一個實例 if (var0 instanceof Continuation) { $continuation = var0; if ((($continuation).label & Integer.MIN_VALUE) != 0) { ($continuation).label -= Integer.MIN_VALUE; break label20; } } //第一次進入走這里, $continuation = new ContinuationImpl(var0) { //協(xié)程返回結果 Object result; //表示協(xié)程狀態(tài)機當前的狀態(tài) int label; //invokeSuspend 是協(xié)程的關鍵 //它最終會調(diào)用 suspendFun(this) 開啟協(xié)程狀態(tài)機 //狀態(tài)機相關代碼就是后面的 switch 語句 //協(xié)程的本質(zhì),可以說就是 CPS + 狀態(tài)機 @Nullable public final Object invokeSuspend(@NotNull Object $result) { this.result = $result; this.label |= Integer.MIN_VALUE; return SuspendDemoKt.suspendFun(this); } }; }
ContinuationImpl是整個協(xié)程掛起函數(shù)的核心,掛起函數(shù)的狀態(tài)機擴展自這個類。
第4行代碼首先判斷了var0是不是Continuation的實例,如果是那就賦值給continuation,首次進入時var0的值是空,因為它還沒有被創(chuàng)建,會進入第13行代碼執(zhí)行,這相當于用一個新的 Continuation 包裝了舊的 Continuation,整個過程中只會創(chuàng)建一個Continuation實例,節(jié)省了內(nèi)存的開銷。
invokeSuspend內(nèi)部取出結果,給label設定初始值,然后開啟協(xié)程的狀態(tài)機,協(xié)程狀態(tài)機的處理過程在switch中
//取出執(zhí)行的結果 Object $result = $continuation.result; //返回是否被掛起的狀態(tài) Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch($continuation.label) { case 0: //異常判斷 ResultKt.throwOnFailure($result); //這里將label的狀態(tài)改成1,進入下一行delay(1000L)代碼 $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 "100000";
創(chuàng)建了Continuation的實例并且給result和label分別賦值,然后就是取出值了,switch是以label為依據(jù)進行處理的:
- case 0:在這里面首先進行異常判斷,如果結果是失敗,則拋出異常。然后這里將狀態(tài)label改為1便于進入下一步處理,因為代碼中第一行就是delay(1000L)所以在label = 0的時候就要去處理延遲函數(shù)的邏輯了:
DelayKt.delay是一個掛起函數(shù),傳入的參數(shù)分別是延遲時間和continuation的實例
DelayKt.delay函數(shù)在內(nèi)部處理完畢后返回了IntrinsicsKt.COROUTINE_SUSPENDED
,這個值就是是否被掛起的標志,與var3進行判斷,條件滿足返回var3,case 0執(zhí)行完畢進入case 1;
- case 1:進入case 1的第一步人就是判斷是否有異常,然后因為原始代碼中delay函數(shù)執(zhí)行完畢后就立即返回了一個“100000”,所以case 1的代碼也就到此為止。
以上就是對反編譯代碼的一個分析,因為原始代碼比較簡單因此反編譯后的代碼分析起來也相對簡單,那么這里簡單總結一下:
- switch實現(xiàn)了協(xié)程狀態(tài)機,里面除了對不同情況下的狀態(tài)的處理外還對狀態(tài)進行了賦值的操作;
- continuation.label是狀態(tài)流轉的關鍵,continuation.label每改變一次就代表了掛起函數(shù)被調(diào)用了一次;
- 每次掛起函數(shù)執(zhí)行完畢后都會檢查是否發(fā)生異常;
- 如果一個函數(shù)被掛起了,它的返回值會是 CoroutineSingletons.COROUTINE_SUSPENDED;
上面的代碼很簡單,現(xiàn)在用一個較為復雜的代碼再進行分析,驗證一下上面總結的幾點內(nèi)容:
原始代碼
suspend fun main() { val provincesCode = getProvincesCode() val cityCode = getCityCode(provincesCode) val areaCode = getAreaCode(cityCode) } /** * 獲取省份Code * */ private suspend fun getProvincesCode(): String { withContext(Dispatchers.IO) { delay(1000L) } return "省:100000" } /** * 獲取城市Code * * @param provincesCode */ private suspend fun getCityCode(provincesCode: String): String { withContext(Dispatchers.IO) { delay(1000L) } return "$provincesCode 市:100010" } /** * 獲取區(qū)域code * * @param cityCode */ private suspend fun getAreaCode(cityCode: String): String { withContext(Dispatchers.IO) { delay(1000L) } return "$cityCode 區(qū):100011" }
上面的代碼反編譯后的代碼讀起來更費勁,這里不對getProvincesCode()
、getCityCode(provincesCode)
、getAreaCode(cityCode)
三個函數(shù)進行分析因為跟上面的那段代碼極為相似,這里主要分析main
函數(shù)中調(diào)用的邏輯:
public static final Object main(@NotNull Continuation var0) { Object $continuation; label37: { if (var0 instanceof <undefinedtype>) { $continuation = (<undefinedtype>)var0; if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) { ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE; break label37; } } $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 RequestCodeKt.main((Continuation)this); } }; } Object var10000; label31: { Object var6; label30: { Object $result = ((<undefinedtype>)$continuation).result; var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); switch(((<undefinedtype>)$continuation).label) { case 0: ResultKt.throwOnFailure($result); ((<undefinedtype>)$continuation).label = 1; var10000 = getProvincesCode((Continuation)$continuation); if (var10000 == var6) { return var6; } break; case 1: ResultKt.throwOnFailure($result); var10000 = $result; break; case 2: ResultKt.throwOnFailure($result); var10000 = $result; break label30; case 3: ResultKt.throwOnFailure($result); var10000 = $result; break label31; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } String provincesCode = (String)var10000; ((<undefinedtype>)$continuation).label = 2; var10000 = getCityCode(provincesCode, (Continuation)$continuation); if (var10000 == var6) { return var6; } } String cityCode = (String)var10000; ((<undefinedtype>)$continuation).label = 3; var10000 = getAreaCode(cityCode, (Continuation)$continuation); if (var10000 == var6) { return var6; } } String var3 = (String)var10000; return Unit.INSTANCE; }
這里的代碼跟上面那個極為相似,保證只創(chuàng)建一個Continuation實例,然后通過label、var6、var10000做出不同的處理
- var6:掛起標志,返回
IntrinsicsKt.getCOROUTINE_SUSPENDED();
- var10000:
getProvincesCode()
、getCityCode(provincesCode)
、getAreaCode(cityCode)
都是掛起函數(shù),因此返回結果中有執(zhí)行結果和掛起標志; - label:label=1、2、3的情況主要都是在調(diào)用一個掛起函數(shù)的手被賦值,這也印證了上面總結的第二天條內(nèi)容;
- switch:這個switch的流轉仍舊是依靠label執(zhí)行的,并且每次都會先進行異常判斷。
第二段的代碼分析結果就是對上面結論的驗證,所以說無論復雜與否它的執(zhí)行流程就是那幾個,多進行分析就了解了,這個過程中一定要自己寫,反編譯,然后自己總結才能理解,單純的看其實還是很費勁的。
這里還有一個點要關注一下,就是三個掛起函數(shù)中為什么都傳入了continuation
,這是因為掛起函數(shù)被反編譯后原本的suspend變成了Continueation參數(shù),因此main函數(shù)也就必須是掛起函數(shù),所以為什么說普通函數(shù)不能調(diào)用掛起函數(shù),就是因為沒有Continuation這個參數(shù)。
5.非掛起函數(shù)的分析
前面在分析CPS轉換后返回值為什么是Any?時提出過非掛起函數(shù),那么非掛起函數(shù)的處理流程是怎樣的呢,將上面的代碼進行修改,保留suspend,刪除掛起函數(shù)的相關代碼:
suspend fun main() { val provincesCode = getProvincesCode() val cityCode = getCityCode(provincesCode) val areaCode = getAreaCode(cityCode) } /** * 獲取省份Code * */ private suspend fun getProvincesCode(): String { withContext(Dispatchers.IO) { delay(1000L) } return "?。?00000" } /** * 獲取城市Code * * @param provincesCode */ private suspend fun getCityCode(provincesCode: String): String { //變化在這里,刪除了withContext和delay函數(shù) return "$provincesCode 市:100010" } /** * 獲取區(qū)域code * * @param cityCode */ private suspend fun getAreaCode(cityCode: String): String { withContext(Dispatchers.IO) { delay(1000L) } return "$cityCode 區(qū):100011" }
反編譯后的代碼唯一變化點在getCityCode
private static final Object getCityCode(String provincesCode, Continuation $completion) { return provincesCode + " 市:100010"; }
反編譯后的代碼變得極為簡單,在getCityCode函數(shù)中沒有了狀態(tài)機的流轉而是直接返回了結果。
以上內(nèi)容就是掛起函數(shù)的執(zhí)行流程,那么它的原理用一句話總結:Kotlin的掛起函數(shù)本質(zhì)上就是一個狀態(tài)機;
以上就是Kotlin 掛起函數(shù)CPS轉換原理解析的詳細內(nèi)容,更多關于Kotlin 掛起函數(shù)CPS轉換的資料請關注腳本之家其它相關文章!
相關文章
Android實現(xiàn)菜單關聯(lián)activity的方法示例
這篇文章主要介紹了Android實現(xiàn)菜單關聯(lián)activity的方法,涉及Android使用Intent實現(xiàn)菜單關聯(lián)activity相關操作技巧,需要的朋友可以參考下2019-03-03Android?Studio實現(xiàn)購買售賣系統(tǒng)
這篇文章主要為大家詳細介紹了Android?Studio實現(xiàn)購買售賣系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02Android Studio中配置OpenCV庫開發(fā)環(huán)境的教程
這篇文章主要介紹了Android Studio中配置OpenCV庫開發(fā)環(huán)境的教程,OpenCV有Java接口,因而也經(jīng)常被用來做安卓開發(fā),需要的朋友可以參考下2016-05-05Android ExpandableListView展開列表控件使用實例
這篇文章主要介紹了Android ExpandableListView展開列表控件使用實例,本文實現(xiàn)了一個類似手機QQ好友列表的界面效果,需要的朋友可以參考下2014-07-07