日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Kotlin?掛起函數CPS轉換原理解析_Android

作者:無糖可樂愛好者 ? 更新時間: 2023-01-05 編程語言

正文

普通函數加上suspend之后就成為了一個掛起函數,Kotlin編譯器會將這個掛起函數轉換成了帶有參數Continuation<T>的一個普通函數,Continuation是一個接口,它跟Java中的Callback有著一樣的功能,這個轉換過程被稱為CPS轉換。

1.什么是CPS轉換

掛起函數中的CPS轉換就是把掛起函數轉換成一個帶有Callback的函數,這里的 Callback 就是 Continuation 接口。在這個過程中會發生函數參數的變化和函數返回值的變化。

suspend fun getAreaCode(): String {
    delay(1000L)
    return "100011"
}
//函數參數的變化
suspend ()變成了(Continuation) 
//函數返回值的變化
-> String變成了 ->Any?
//變化后的代碼如下
private fun getProvinceCode(c: Continuation<String>): Any? {
    return "100000"
}

2.CPS的過程是怎么讓參數改變的

這個問題的答案其實在掛起函數哪里提到過,Kotlin代碼可以運行主要是Kotlin編譯器將代碼轉換成了Java字節碼,然后交給Java虛擬機執行,那么轉換成Java后的掛起函數就是一個帶有Callback回調的普通函數,對應Kotlin的話就是Continuation函數,那么這是參數的改變,代碼的轉換就是:

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"
}

這里就可以解答一個疑問:為什么普通函數不可以調用掛起函數了? 這是因為掛起函數被Kotlin編譯器便后默認是需要傳入一個Continuation參數的,而普通函數沒有這個類型的參數。

3.CPS的過程是怎么讓返回值改變的

原本的代碼是返回了一個String類型的值,但是通過CPS轉換后String變成了Any?,如果說String是Any?的子類這樣也行的通,但是String為什么沒了呢,以及為什么會多了一個Any?

首先解釋這個String為什么沒有了,其實String不是沒有了,而是換了個地方

//											換到了這里
private fun getProvinceCode(c: Continuation<String>): Any? {
    return "100000"
}

CPS轉換它必定是一個等價交換, 否則編譯后的程序就失去了原本的作用,也就是說這個String它會以另一種形式存在。

現在解釋第二個問題,為什么會多了一個Any?

掛起函數經過 CPS 轉換后,它的返回值有一個重要作用:標志該掛起函數有沒有被掛起。 掛起函數也有可能不會被掛起,上面的掛起函數中都添加了delay(1000L),而delay(1000L)是一個掛起函數這個是已經知道的,那么如果不加它會怎么樣呢

上面的函數刪除了delay(1000L)只有suspend成了灰色并且提示信息:suspend是多余的, 用兩段代碼做個對比

//有效的掛起函數
private suspend fun suspendFun(): String {
    delay(1000L)
    return "100000"
}
//無效的掛起函數
private suspend fun noSuspendFun(): String {
    return "100000"
}

反編譯后的Java代碼

//函數調用
@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));
}
//有效的掛起函數
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";
}
//無效的掛起函數
private static final Object noSuspendFun(Continuation $completion) {
    return "100000";
}

通過代碼可以很清楚的看到suspendFunnoSuspendFun兩個函數的區別,返回值可能是IntrinsicsKt.getCOROUTINE_SUSPENDED()也有可能是var10000 也可能是Unit.INSTANCE,也有可能是一個null,因此為了滿足所有可能性使用Any?是最合適的

為什么說Any? 是最合適的?

Kotlin中的Any類似于Java中的Object,Any是不可為空的,Any?是可以為空的,Any?包含Any的同時還包含了可空的類型,也就是說后者的包容性比前者更廣,所以說前者就是后者的子類,同樣的String和String?、Unit和Unit?也是一樣的關系,用圖表示就是這樣

4.掛起函數的反編譯

這里直接將上面suspendFun函數反編譯后的代碼拿來分析

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) {
            //協程返回結果
            Object result;
            //表示協程狀態機當前的狀態
            int label;
            //invokeSuspend 是協程的關鍵
            //它最終會調用 suspendFun(this) 開啟協程狀態機
            //狀態機相關代碼就是后面的 switch 語句
            //協程的本質,可以說就是 CPS + 狀態機
            @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);
            //這里將label的狀態改成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";
   }

這里先對幾個變量、函數進行說明:

  • undefinedtype根據上下問的代碼可以輕松的推斷出來就是Continuation;
  • label 是用來代表協程狀態機當中狀態的;
  • result 是用來存儲當前掛起函數執行結果的;
  • invokeSuspend 這個函數,是整個狀態機的入口,它會將執行流程轉交給 suspendFun() 進行再次調用。

反編譯的代碼讀起來比較費勁,因為原本提供的掛起函數代碼的例子比較簡單所以慢慢分析的話還是比較好理解的。

這里首先分析第一段代碼的作用,根據上面的注釋我將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) {
        //協程返回結果
        Object result;
        //表示協程狀態機當前的狀態
        int label;
        //invokeSuspend 是協程的關鍵
        //它最終會調用 suspendFun(this) 開啟協程狀態機
        //狀態機相關代碼就是后面的 switch 語句
        //協程的本質,可以說就是 CPS + 狀態機
        @Nullable
        public final Object invokeSuspend(@NotNull Object $result) {
            this.result = $result;
            this.label |= Integer.MIN_VALUE;
            return SuspendDemoKt.suspendFun(this);
        }
    };
}

ContinuationImpl是整個協程掛起函數的核心,掛起函數的狀態機擴展自這個類。

第4行代碼首先判斷了var0是不是Continuation的實例,如果是那就賦值給continuation,首次進入時var0的值是空,因為它還沒有被創建,會進入第13行代碼執行,這相當于用一個新的 Continuation 包裝了舊的 Continuation,整個過程中只會創建一個Continuation實例,節省了內存的開銷。

invokeSuspend內部取出結果,給label設定初始值,然后開啟協程的狀態機,協程狀態機的處理過程在switch中

//取出執行的結果
Object $result = $continuation.result;
//返回是否被掛起的狀態
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch($continuation.label) {
    case 0:
        //異常判斷
        ResultKt.throwOnFailure($result);
        //這里將label的狀態改成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";

創建了Continuation的實例并且給result和label分別賦值,然后就是取出值了,switch是以label為依據進行處理的:

  • case 0:在這里面首先進行異常判斷,如果結果是失敗,則拋出異常。然后這里將狀態label改為1便于進入下一步處理,因為代碼中第一行就是delay(1000L)所以在label = 0的時候就要去處理延遲函數的邏輯了:

DelayKt.delay是一個掛起函數,傳入的參數分別是延遲時間和continuation的實例

DelayKt.delay函數在內部處理完畢后返回了IntrinsicsKt.COROUTINE_SUSPENDED,這個值就是是否被掛起的標志,與var3進行判斷,條件滿足返回var3,case 0執行完畢進入case 1;

  • case 1:進入case 1的第一步人就是判斷是否有異常,然后因為原始代碼中delay函數執行完畢后就立即返回了一個“100000”,所以case 1的代碼也就到此為止。

以上就是對反編譯代碼的一個分析,因為原始代碼比較簡單因此反編譯后的代碼分析起來也相對簡單,那么這里簡單總結一下:

  • switch實現了協程狀態機,里面除了對不同情況下的狀態的處理外還對狀態進行了賦值的操作;
  • continuation.label是狀態流轉的關鍵,continuation.label每改變一次就代表了掛起函數被調用了一次;
  • 每次掛起函數執行完畢后都會檢查是否發生異常;
  • 如果一個函數被掛起了,它的返回值會是 CoroutineSingletons.COROUTINE_SUSPENDED;

上面的代碼很簡單,現在用一個較為復雜的代碼再進行分析,驗證一下上面總結的幾點內容:

原始代碼

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"
}
/**
 * 獲取區域code
 *
 * @param cityCode
 */
private suspend fun getAreaCode(cityCode: String): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "$cityCode 區:100011"
}

上面的代碼反編譯后的代碼讀起來更費勁,這里不對getProvincesCode()getCityCode(provincesCode)getAreaCode(cityCode)三個函數進行分析因為跟上面的那段代碼極為相似,這里主要分析main函數中調用的邏輯:

public static final Object main(@NotNull Continuation var0) {
    Object $continuation;
    label37: {
        if (var0 instanceof &lt;undefinedtype&gt;) {
            $continuation = (&lt;undefinedtype&gt;)var0;
            if ((((&lt;undefinedtype&gt;)$continuation).label &amp; Integer.MIN_VALUE) != 0) {
                ((&lt;undefinedtype&gt;)$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 = ((&lt;undefinedtype&gt;)$continuation).result;
            var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch(((&lt;undefinedtype&gt;)$continuation).label) {
                case 0:
                    ResultKt.throwOnFailure($result);
                    ((&lt;undefinedtype&gt;)$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;
            ((&lt;undefinedtype&gt;)$continuation).label = 2;
            var10000 = getCityCode(provincesCode, (Continuation)$continuation);
            if (var10000 == var6) {
                return var6;
            }
        }
        String cityCode = (String)var10000;
        ((&lt;undefinedtype&gt;)$continuation).label = 3;
        var10000 = getAreaCode(cityCode, (Continuation)$continuation);
        if (var10000 == var6) {
            return var6;
        }
    }
    String var3 = (String)var10000;
    return Unit.INSTANCE;
}

這里的代碼跟上面那個極為相似,保證只創建一個Continuation實例,然后通過label、var6、var10000做出不同的處理

  • var6:掛起標志,返回IntrinsicsKt.getCOROUTINE_SUSPENDED();
  • var10000:getProvincesCode()getCityCode(provincesCode)getAreaCode(cityCode)都是掛起函數,因此返回結果中有執行結果和掛起標志;
  • label:label=1、2、3的情況主要都是在調用一個掛起函數的手被賦值,這也印證了上面總結的第二天條內容;
  • switch:這個switch的流轉仍舊是依靠label執行的,并且每次都會先進行異常判斷。

第二段的代碼分析結果就是對上面結論的驗證,所以說無論復雜與否它的執行流程就是那幾個,多進行分析就了解了,這個過程中一定要自己寫,反編譯,然后自己總結才能理解,單純的看其實還是很費勁的。

這里還有一個點要關注一下,就是三個掛起函數中為什么都傳入了continuation,這是因為掛起函數被反編譯后原本的suspend變成了Continueation參數,因此main函數也就必須是掛起函數,所以為什么說普通函數不能調用掛起函數,就是因為沒有Continuation這個參數。

5.非掛起函數的分析

前面在分析CPS轉換后返回值為什么是Any?時提出過非掛起函數,那么非掛起函數的處理流程是怎樣的呢,將上面的代碼進行修改,保留suspend,刪除掛起函數的相關代碼:

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和delay函數
    return "$provincesCode 市:100010"
}
/**
* 獲取區域code
*
* @param cityCode
*/
private suspend fun getAreaCode(cityCode: String): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "$cityCode 區:100011"
    }

反編譯后的代碼唯一變化點在getCityCode

private static final Object getCityCode(String provincesCode, Continuation $completion) {
    return provincesCode + " 市:100010";
}

反編譯后的代碼變得極為簡單,在getCityCode函數中沒有了狀態機的流轉而是直接返回了結果。

以上內容就是掛起函數的執行流程,那么它的原理用一句話總結:Kotlin的掛起函數本質上就是一個狀態機;

原文鏈接:https://juejin.cn/post/7172798568061730824

欄目分類
最近更新