Kotlin?掛起函數(shù)CPS轉(zhuǎn)換原理解析
正文
普通函數(shù)加上suspend之后就成為了一個(gè)掛起函數(shù),Kotlin編譯器會(huì)將這個(gè)掛起函數(shù)轉(zhuǎn)換成了帶有參數(shù)Continuation<T>的一個(gè)普通函數(shù),Continuation是一個(gè)接口,它跟Java中的Callback有著一樣的功能,這個(gè)轉(zhuǎn)換過程被稱為CPS轉(zhuǎn)換。
1.什么是CPS轉(zhuǎn)換
掛起函數(shù)中的CPS轉(zhuǎn)換就是把掛起函數(shù)轉(zhuǎn)換成一個(gè)帶有Callback的函數(shù),這里的 Callback 就是 Continuation 接口。在這個(gè)過程中會(huì)發(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ù)改變的
這個(gè)問題的答案其實(shí)在掛起函數(shù)哪里提到過,Kotlin代碼可以運(yùn)行主要是Kotlin編譯器將代碼轉(zhuǎn)換成了Java字節(jié)碼,然后交給Java虛擬機(jī)執(zhí)行,那么轉(zhuǎn)換成Java后的掛起函數(shù)就是一個(gè)帶有Callback回調(diào)的普通函數(shù),對(duì)應(yīng)Kotlin的話就是Continuation函數(shù),那么這是參數(shù)的改變,代碼的轉(zhuǎn)換就是:
private suspend fun getProvinceCode(): String {
delay(1000L)
return "100000"
}
/**
* Kotlin轉(zhuǎn)換的Java代碼
*/
private static final Object getProvinceCode(Continuation $completion) {
return "100000";
}
private fun getProvinceCode(c: Continuation<String>): Any? {
return "100000"
}
這里就可以解答一個(gè)疑問:為什么普通函數(shù)不可以調(diào)用掛起函數(shù)了? 這是因?yàn)閽炱鸷瘮?shù)被Kotlin編譯器便后默認(rèn)是需要傳入一個(gè)Continuation參數(shù)的,而普通函數(shù)沒有這個(gè)類型的參數(shù)。
3.CPS的過程是怎么讓返回值改變的
原本的代碼是返回了一個(gè)String類型的值,但是通過CPS轉(zhuǎn)換后String變成了Any?,如果說String是Any?的子類這樣也行的通,但是String為什么沒了呢,以及為什么會(huì)多了一個(gè)Any?
首先解釋這個(gè)String為什么沒有了,其實(shí)String不是沒有了,而是換了個(gè)地方
// 換到了這里
private fun getProvinceCode(c: Continuation<String>): Any? {
return "100000"
}
CPS轉(zhuǎn)換它必定是一個(gè)等價(jià)交換, 否則編譯后的程序就失去了原本的作用,也就是說這個(gè)String它會(huì)以另一種形式存在。
現(xiàn)在解釋第二個(gè)問題,為什么會(huì)多了一個(gè)Any?
掛起函數(shù)經(jīng)過 CPS 轉(zhuǎn)換后,它的返回值有一個(gè)重要作用:標(biāo)志該掛起函數(shù)有沒有被掛起。 掛起函數(shù)也有可能不會(huì)被掛起,上面的掛起函數(shù)中都添加了delay(1000L),而delay(1000L)是一個(gè)掛起函數(shù)這個(gè)是已經(jīng)知道的,那么如果不加它會(huì)怎么樣呢

上面的函數(shù)刪除了delay(1000L)只有suspend成了灰色并且提示信息:suspend是多余的, 用兩段代碼做個(gè)對(duì)比
//有效的掛起函數(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兩個(gè)函數(shù)的區(qū)別,返回值可能是IntrinsicsKt.getCOROUTINE_SUSPENDED()也有可能是var10000 也可能是Unit.INSTANCE,也有可能是一個(gè)null,因此為了滿足所有可能性使用Any?是最合適的
為什么說Any? 是最合適的?
Kotlin中的Any類似于Java中的Object,Any是不可為空的,Any?是可以為空的,Any?包含Any的同時(shí)還包含了可空的類型,也就是說后者的包容性比前者更廣,所以說前者就是后者的子類,同樣的String和String?、Unit和Unit?也是一樣的關(guān)系,用圖表示就是這樣

4.掛起函數(shù)的反編譯
這里直接將上面suspendFun函數(shù)反編譯后的代碼拿來分析
private static final Object suspendFun(Continuation var0) {
Object $continuation;
label20: {
//undefinedtype就是Continuation
//不是第一次進(jìn)入走這里,保證只生成了一個(gè)實(shí)例
if (var0 instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)var0;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
//第一次進(jìn)入走這里,
$continuation = new ContinuationImpl(var0) {
//協(xié)程返回結(jié)果
Object result;
//表示協(xié)程狀態(tài)機(jī)當(dāng)前的狀態(tài)
int label;
//invokeSuspend 是協(xié)程的關(guān)鍵
//它最終會(huì)調(diào)用 suspendFun(this) 開啟協(xié)程狀態(tài)機(jī)
//狀態(tài)機(jī)相關(guān)代碼就是后面的 switch 語句
//協(xié)程的本質(zhì),可以說就是 CPS + 狀態(tài)機(jī)
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return SuspendDemoKt.suspendFun(this);
}
};
}
//取出執(zhí)行的結(jié)果
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,進(jìn)入下一行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";
}
這里先對(duì)幾個(gè)變量、函數(shù)進(jìn)行說明:
- undefinedtype根據(jù)上下問的代碼可以輕松的推斷出來就是Continuation;
- label 是用來代表協(xié)程狀態(tài)機(jī)當(dāng)中狀態(tài)的;
- result 是用來存儲(chǔ)當(dāng)前掛起函數(shù)執(zhí)行結(jié)果的;
- invokeSuspend 這個(gè)函數(shù),是整個(gè)狀態(tài)機(jī)的入口,它會(huì)將執(zhí)行流程轉(zhuǎn)交給 suspendFun() 進(jìn)行再次調(diào)用。
反編譯的代碼讀起來比較費(fèi)勁,因?yàn)樵咎峁┑膾炱鸷瘮?shù)代碼的例子比較簡(jiǎn)單所以慢慢分析的話還是比較好理解的。
這里首先分析第一段代碼的作用,根據(jù)上面的注釋我將undefinedtype修改為Continueation
label20: {
//undefinedtype就是Continuation
//不是第一次進(jìn)入走這里,保證只生成了一個(gè)實(shí)例
if (var0 instanceof Continuation) {
$continuation = var0;
if ((($continuation).label & Integer.MIN_VALUE) != 0) {
($continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
//第一次進(jìn)入走這里,
$continuation = new ContinuationImpl(var0) {
//協(xié)程返回結(jié)果
Object result;
//表示協(xié)程狀態(tài)機(jī)當(dāng)前的狀態(tài)
int label;
//invokeSuspend 是協(xié)程的關(guān)鍵
//它最終會(huì)調(diào)用 suspendFun(this) 開啟協(xié)程狀態(tài)機(jī)
//狀態(tài)機(jī)相關(guān)代碼就是后面的 switch 語句
//協(xié)程的本質(zhì),可以說就是 CPS + 狀態(tài)機(jī)
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return SuspendDemoKt.suspendFun(this);
}
};
}
ContinuationImpl是整個(gè)協(xié)程掛起函數(shù)的核心,掛起函數(shù)的狀態(tài)機(jī)擴(kuò)展自這個(gè)類。
第4行代碼首先判斷了var0是不是Continuation的實(shí)例,如果是那就賦值給continuation,首次進(jìn)入時(shí)var0的值是空,因?yàn)樗€沒有被創(chuàng)建,會(huì)進(jìn)入第13行代碼執(zhí)行,這相當(dāng)于用一個(gè)新的 Continuation 包裝了舊的 Continuation,整個(gè)過程中只會(huì)創(chuàng)建一個(gè)Continuation實(shí)例,節(jié)省了內(nèi)存的開銷。
invokeSuspend內(nèi)部取出結(jié)果,給label設(shè)定初始值,然后開啟協(xié)程的狀態(tài)機(jī),協(xié)程狀態(tài)機(jī)的處理過程在switch中
//取出執(zhí)行的結(jié)果
Object $result = $continuation.result;
//返回是否被掛起的狀態(tài)
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch($continuation.label) {
case 0:
//異常判斷
ResultKt.throwOnFailure($result);
//這里將label的狀態(tài)改成1,進(jìn)入下一行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的實(shí)例并且給result和label分別賦值,然后就是取出值了,switch是以label為依據(jù)進(jìn)行處理的:
- case 0:在這里面首先進(jìn)行異常判斷,如果結(jié)果是失敗,則拋出異常。然后這里將狀態(tài)label改為1便于進(jìn)入下一步處理,因?yàn)榇a中第一行就是delay(1000L)所以在label = 0的時(shí)候就要去處理延遲函數(shù)的邏輯了:
DelayKt.delay是一個(gè)掛起函數(shù),傳入的參數(shù)分別是延遲時(shí)間和continuation的實(shí)例
DelayKt.delay函數(shù)在內(nèi)部處理完畢后返回了IntrinsicsKt.COROUTINE_SUSPENDED,這個(gè)值就是是否被掛起的標(biāo)志,與var3進(jìn)行判斷,條件滿足返回var3,case 0執(zhí)行完畢進(jìn)入case 1;
- case 1:進(jìn)入case 1的第一步人就是判斷是否有異常,然后因?yàn)樵即a中delay函數(shù)執(zhí)行完畢后就立即返回了一個(gè)“100000”,所以case 1的代碼也就到此為止。
以上就是對(duì)反編譯代碼的一個(gè)分析,因?yàn)樵即a比較簡(jiǎn)單因此反編譯后的代碼分析起來也相對(duì)簡(jiǎn)單,那么這里簡(jiǎn)單總結(jié)一下:
- switch實(shí)現(xiàn)了協(xié)程狀態(tài)機(jī),里面除了對(duì)不同情況下的狀態(tài)的處理外還對(duì)狀態(tài)進(jìn)行了賦值的操作;
- continuation.label是狀態(tài)流轉(zhuǎn)的關(guān)鍵,continuation.label每改變一次就代表了掛起函數(shù)被調(diào)用了一次;
- 每次掛起函數(shù)執(zhí)行完畢后都會(huì)檢查是否發(fā)生異常;
- 如果一個(gè)函數(shù)被掛起了,它的返回值會(huì)是 CoroutineSingletons.COROUTINE_SUSPENDED;
上面的代碼很簡(jiǎn)單,現(xiàn)在用一個(gè)較為復(fù)雜的代碼再進(jìn)行分析,驗(yàn)證一下上面總結(jié)的幾點(diǎ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 "?。?00000"
}
/**
* 獲取城市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"
}
上面的代碼反編譯后的代碼讀起來更費(fèi)勁,這里不對(duì)getProvincesCode() 、getCityCode(provincesCode)、getAreaCode(cityCode)三個(gè)函數(shù)進(jìn)行分析因?yàn)楦厦娴哪嵌未a極為相似,這里主要分析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;
}
這里的代碼跟上面那個(gè)極為相似,保證只創(chuàng)建一個(gè)Continuation實(shí)例,然后通過label、var6、var10000做出不同的處理
- var6:掛起標(biāo)志,返回
IntrinsicsKt.getCOROUTINE_SUSPENDED(); - var10000:
getProvincesCode()、getCityCode(provincesCode)、getAreaCode(cityCode)都是掛起函數(shù),因此返回結(jié)果中有執(zhí)行結(jié)果和掛起標(biāo)志; - label:label=1、2、3的情況主要都是在調(diào)用一個(gè)掛起函數(shù)的手被賦值,這也印證了上面總結(jié)的第二天條內(nèi)容;
- switch:這個(gè)switch的流轉(zhuǎn)仍舊是依靠label執(zhí)行的,并且每次都會(huì)先進(jìn)行異常判斷。
第二段的代碼分析結(jié)果就是對(duì)上面結(jié)論的驗(yàn)證,所以說無論復(fù)雜與否它的執(zhí)行流程就是那幾個(gè),多進(jìn)行分析就了解了,這個(gè)過程中一定要自己寫,反編譯,然后自己總結(jié)才能理解,單純的看其實(shí)還是很費(fèi)勁的。
這里還有一個(gè)點(diǎn)要關(guān)注一下,就是三個(gè)掛起函數(shù)中為什么都傳入了continuation,這是因?yàn)閽炱鸷瘮?shù)被反編譯后原本的suspend變成了Continueation參數(shù),因此main函數(shù)也就必須是掛起函數(shù),所以為什么說普通函數(shù)不能調(diào)用掛起函數(shù),就是因?yàn)闆]有Continuation這個(gè)參數(shù)。
5.非掛起函數(shù)的分析
前面在分析CPS轉(zhuǎn)換后返回值為什么是Any?時(shí)提出過非掛起函數(shù),那么非掛起函數(shù)的處理流程是怎樣的呢,將上面的代碼進(jìn)行修改,保留suspend,刪除掛起函數(shù)的相關(guān)代碼:
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"
}
反編譯后的代碼唯一變化點(diǎn)在getCityCode
private static final Object getCityCode(String provincesCode, Continuation $completion) {
return provincesCode + " 市:100010";
}
反編譯后的代碼變得極為簡(jiǎn)單,在getCityCode函數(shù)中沒有了狀態(tài)機(jī)的流轉(zhuǎn)而是直接返回了結(jié)果。
以上內(nèi)容就是掛起函數(shù)的執(zhí)行流程,那么它的原理用一句話總結(jié):Kotlin的掛起函數(shù)本質(zhì)上就是一個(gè)狀態(tài)機(jī);

以上就是Kotlin 掛起函數(shù)CPS轉(zhuǎn)換原理解析的詳細(xì)內(nèi)容,更多關(guān)于Kotlin 掛起函數(shù)CPS轉(zhuǎn)換的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)菜單關(guān)聯(lián)activity的方法示例
這篇文章主要介紹了Android實(shí)現(xiàn)菜單關(guān)聯(lián)activity的方法,涉及Android使用Intent實(shí)現(xiàn)菜單關(guān)聯(lián)activity相關(guān)操作技巧,需要的朋友可以參考下2019-03-03
Android?Studio實(shí)現(xiàn)購買售賣系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Android?Studio實(shí)現(xiàn)購買售賣系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02
Android TextView字體顏色設(shè)置方法小結(jié)
這篇文章主要介紹了Android TextView字體顏色設(shè)置方法,結(jié)合實(shí)例形式總結(jié)分析了Android開發(fā)中TextView設(shè)置字體顏色的常用技巧,需要的朋友可以參考下2016-02-02
Android Studio中配置OpenCV庫開發(fā)環(huán)境的教程
這篇文章主要介紹了Android Studio中配置OpenCV庫開發(fā)環(huán)境的教程,OpenCV有Java接口,因而也經(jīng)常被用來做安卓開發(fā),需要的朋友可以參考下2016-05-05
Android ExpandableListView展開列表控件使用實(shí)例
這篇文章主要介紹了Android ExpandableListView展開列表控件使用實(shí)例,本文實(shí)現(xiàn)了一個(gè)類似手機(jī)QQ好友列表的界面效果,需要的朋友可以參考下2014-07-07

