首頁 > 軟體

Kotlin掛起函數原理範例剖析

2022-08-04 18:02:03

一、序言

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掛起函數的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com