首頁 > 軟體

Kotlin掛起函數應用介紹

2022-11-23 14:01:29

學習了極客時間課程,記錄下學習輸出。

一、CPS轉換

掛起函數,比普通的函數多了 suspend 關鍵字。通過suspend 關鍵字,Kotlin 編譯器就會特殊對待這個函數,將其轉換成一個帶有 Callback 的函數,這裡的 Callback 就是 Continuation 介面。

例、CPS 轉換:

suspend fun getUserInfo(): Any {
    return "UserInfo"
}
----->
fun getUserInfo(ct:Continuation): Any? {
    ct.resumeWith("UserInfo")
    return Unit
}

PS 轉換過程中,函數的型別發生了變化:suspend ()->Any 變成了 (Continuation)-> Any?。這意味著,如果你在 Java 裡存取一個 Kotlin 掛起函數 getUserInfo(),會看到 getUserInfo() 的型別是 (Continuation)-> Object,接收 Continuation 為引數,返回值是 Object。而在這裡,函數簽名的變化可以分為兩個部分:函數簽名的變化可以分為兩個部分:函數引數的變化和函數返回值的變化。

1.CPS 引數變化

suspend() 變成 (Continuation)

suspend fun getUserInfoContent(): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "UserInfo"
}
suspend fun getFriendListContent(user: String): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "Friend1, Friend2"
}
suspend fun getFeedListContent(user: String, list: String): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "{FeddList...}"
}
suspend fun fetchContent() {
    val userInfoContent = getUserInfoContent()
    val friendListContent = getFriendListContent(userInfoContent)
    val feedListContent = getFeedListContent(userInfoContent, friendListContent)
}

上述程式碼轉換成java程式碼如下:

public final class TestCoroutionKt {
   @Nullable
   public static final Object getUserInfoContent(@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 TestCoroutionKt.getUserInfoContent(this);
            }
         };
      }
      Object $result = ((<undefinedtype>)$continuation).result;
      Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch(((<undefinedtype>)$continuation).label) {
      case 0:
         ResultKt.throwOnFailure($result);
         CoroutineContext var10000 = (CoroutineContext)Dispatchers.getIO();
         Function2 var10001 = (Function2)(new Function2((Continuation)null) {
            int label;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               switch(this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  this.label = 1;
                  if (DelayKt.delay(1000L, this) == var2) {
                     return var2;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
               }
               return Unit.INSTANCE;
            }
            @NotNull
            public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
               Intrinsics.checkNotNullParameter(completion, "completion");
               Function2 var3 = new <anonymous constructor>(completion);
               return var3;
            }
            public final Object invoke(Object var1, Object var2) {
               return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
            }
         });
         ((<undefinedtype>)$continuation).label = 1;
         if (BuildersKt.withContext(var10000, var10001, (Continuation)$continuation) == var3) {
            return var3;
         }
         break;
      case 1:
         ResultKt.throwOnFailure($result);
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }
      return "UserInfo";
   }
   @Nullable
   public static final Object getFriendListContent(@NotNull String var0, @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 TestCoroutionKt.getFriendListContent((String)null, this);
            }
         };
      }
      Object $result = ((<undefinedtype>)$continuation).result;
      Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch(((<undefinedtype>)$continuation).label) {
      case 0:
         ResultKt.throwOnFailure($result);
         CoroutineContext var10000 = (CoroutineContext)Dispatchers.getIO();
         Function2 var10001 = (Function2)(new Function2((Continuation)null) {
            int label;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               switch(this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  this.label = 1;
                  if (DelayKt.delay(1000L, this) == var2) {
                     return var2;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
               }
               return Unit.INSTANCE;
            }
            @NotNull
            public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
               Intrinsics.checkNotNullParameter(completion, "completion");
               Function2 var3 = new <anonymous constructor>(completion);
               return var3;
            }
            public final Object invoke(Object var1, Object var2) {
               return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
            }
         });
         ((<undefinedtype>)$continuation).label = 1;
         if (BuildersKt.withContext(var10000, var10001, (Continuation)$continuation) == var4) {
            return var4;
         }
         break;
      case 1:
         ResultKt.throwOnFailure($result);
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }
      return "Friend1, Friend2";
   }
   @Nullable
   public static final Object getFeedListContent(@NotNull String var0, @NotNull String var1, @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 TestCoroutionKt.getFeedListContent((String)null, (String)null, this);
            }
         };
      }
      Object $result = ((<undefinedtype>)$continuation).result;
      Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch(((<undefinedtype>)$continuation).label) {
      case 0:
         ResultKt.throwOnFailure($result);
         CoroutineContext var10000 = (CoroutineContext)Dispatchers.getIO();
         Function2 var10001 = (Function2)(new Function2((Continuation)null) {
            int label;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               switch(this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  this.label = 1;
                  if (DelayKt.delay(1000L, this) == var2) {
                     return var2;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
               }
               return Unit.INSTANCE;
            }
            @NotNull
            public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
               Intrinsics.checkNotNullParameter(completion, "completion");
               Function2 var3 = new <anonymous constructor>(completion);
               return var3;
            }
            public final Object invoke(Object var1, Object var2) {
               return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
            }
         });
         ((<undefinedtype>)$continuation).label = 1;
         if (BuildersKt.withContext(var10000, var10001, (Continuation)$continuation) == var5) {
            return var5;
         }
         break;
      case 1:
         ResultKt.throwOnFailure($result);
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }
      return "{FeddList...}";
   }
   @Nullable
   public static final Object fetchContent(@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;
            Object L$0;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return TestCoroutionKt.fetchContent(this);
            }
         };
      }
      Object var10000;
      label31: {
         String userInfoContent;
         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 = getUserInfoContent((Continuation)$continuation);
               if (var10000 == var6) {
                  return var6;
               }
               break;
            case 1:
               ResultKt.throwOnFailure($result);
               var10000 = $result;
               break;
            case 2:
               userInfoContent = (String)((<undefinedtype>)$continuation).L$0;
               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");
            }
            userInfoContent = (String)var10000;
            ((<undefinedtype>)$continuation).L$0 = userInfoContent;
            ((<undefinedtype>)$continuation).label = 2;
            var10000 = getFriendListContent(userInfoContent, (Continuation)$continuation);
            if (var10000 == var6) {
               return var6;
            }
         }
         String friendListContent = (String)var10000;
         ((<undefinedtype>)$continuation).L$0 = null;
         ((<undefinedtype>)$continuation).label = 3;
         var10000 = getFeedListContent(userInfoContent, friendListContent, (Continuation)$continuation);
         if (var10000 == var6) {
            return var6;
         }
      }
      String var3 = (String)var10000;
      return Unit.INSTANCE;
   }
}

每一次函數呼叫的時候,continuation 都會作為最後一個引數傳到掛起函數裡,Kotlin 編譯器幫我們做的,我們開發者是無感知。

2.CPS 返回值變化

final Object getUserInfoContent(@NotNull Continuation var0)
final Object getFriendListContent(@NotNull String var0, @NotNull Continuation var1)
final Object getFeedListContent(@NotNull String var0, @NotNull String var1, @NotNull Continuation var2)
suspend fun getUserInfoContent(): String {}
fun getUserInfoContent(cont: Continuation): Any? {}

經過 CPS 轉換後,完整的函數簽名如下:

suspend fun getUserInfoContent(): String {}
fun getUserInfoContent(cont: Continuation<String>): Any? {}

Kotlin 編譯器的 CPS 轉換是等價的轉換。suspend () -> String 轉換成 (Continuation) -> Any?。

掛起函數經過 CPS 轉換後,它的返回值有一個重要作用:標誌該掛起函數有沒有被掛起。

其實掛起函數也能不被掛起。

首先只要有 suspend 修飾的函數,它就是掛起函數。

suspend fun getUserInfoContent(): String {
    withContext(Dispatchers.IO) {
        delay(1000L)
    }
    return "UserInfo"
}

執行到 withContext{} 的時候,就會返回 CoroutineSingletons.COROUTINE_SUSPENDED 表示函數被掛起了。

下面的函數則是偽掛起函數

suspend fun getUserInfoContent2(): String {
    return "UserInfo"
}

因為它的方法體跟普通函數一樣。它跟一般的掛起函數有個區別:在執行的時候,它並不會被掛起,因為它就是個普通函數。

二、掛起函數的反編譯

 @Nullable
   public static final Object fetchContent(@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;
            Object L$0;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return TestCoroutionKt.fetchContent(this);
            }
         };
      }
      Object var10000;
      label31: {
         String userInfoContent;
         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 = getUserInfoContent((Continuation)$continuation);
               if (var10000 == var6) {
                  return var6;
               }
               break;
            case 1:
               ResultKt.throwOnFailure($result);
               var10000 = $result;
               break;
            case 2:
               userInfoContent = (String)((<undefinedtype>)$continuation).L$0;
               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");
            }
            userInfoContent = (String)var10000;
            ((<undefinedtype>)$continuation).L$0 = userInfoContent;
            ((<undefinedtype>)$continuation).label = 2;
            var10000 = getFriendListContent(userInfoContent, (Continuation)$continuation);
            if (var10000 == var6) {
               return var6;
            }
         }
         String friendListContent = (String)var10000;
         ((<undefinedtype>)$continuation).L$0 = null;
         ((<undefinedtype>)$continuation).label = 3;
         var10000 = getFeedListContent(userInfoContent, friendListContent, (Continuation)$continuation);
         if (var10000 == var6) {
            return var6;
         }
      }
      String var3 = (String)var10000;
      return Unit.INSTANCE;
   }

label 是用來代表協程狀態機當中狀態;

result 是用來儲存當前掛起函數執行結果;

invokeSuspend 這個函數,是整個狀態機的入口,它會將執行流程轉交給 fetchContent 進行再次呼叫;

userInfoContent, friendListContent用來儲存歷史掛起函數執行結果。

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;
            Object L$0;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return TestCoroutionKt.fetchContent(this);
            }
         };

invokeSuspend 最終會呼叫 fetchContent;

如果是初次執行,會建立一個 ContinuationImpl物件,completion 作為引數;這相當於用一個新的 Continuation 包裝了舊的 Continuation;

如果不是初次執行,直接將 completion 賦值給 continuation;這說明 continuation 在整個執行期間,只會產生一個範例,這能極大地節省記憶體開銷(對比 CallBack)。

// result 接收協程的執行結果
var result = continuation.result
// suspendReturn 接收掛起函數的返回值
var suspendReturn: Any? = null
// CoroutineSingletons 是個列舉類
// COROUTINE_SUSPENDED 代表當前函數被掛起了
val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED

continuation.label 是狀態流轉的關鍵,continuation.label 改變一次,就代表了掛起函數被呼叫了一次;每次掛起函數執行完後,都會檢查是否發生異常;

fetchContent 裡的原本的程式碼,被拆分到狀態機裡各個狀態中,分開執行;getUserInfoContent(continuation)、getFriendListContent(user, continuation)、getFeedListContent(friendList, continuation) 三個函數呼叫的是同一個 continuation 範例;

var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();

如果一個函數被掛起了,它的返回值會是 CoroutineSingletons.COROUTINE_SUSPENDED;

在掛起函數執行的過程中,狀態機會把之前的結果以成員變數的方式儲存在 continuation 中。

本質上來說,Kotlin 協程就是通過 label 程式碼段巢狀,配合 switch 巧妙構造出一個狀態機結構。

三、Continuation

public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}
@Suppress("WRONG_MODIFIER_TARGET")
public suspend inline val coroutineContext: CoroutineContext
    get() {
        throw NotImplementedError("Implemented as intrinsic")
    }

注意上面的suspend inline val coroutineContext,suspend 的這種用法只是一種特殊用法。它的作用:它是一個只有在掛起函數作用域下,才能存取的頂層的不可變的變數。這裡的 inline,意味著它的具體實現會被直接複製到程式碼的呼叫處。

suspend fun testContext() = coroutineContext
@Nullable
   public static final Object testContext(@NotNull Continuation $completion) {
      return $completion.getContext();
   }

“suspend inline val coroutineContext”,本質上就是 Kotlin 官方提供的一種方便開發者在掛起函數當中,獲取協程上下文的手段。它的具體實現,其實是 Kotlin 編譯器來完成的。

我們在掛起函數當中無法直接存取 Continuation 物件,但可以存取到 Continuation 當中的 coroutineContext。要知道,正常情況下,我們想要存取 Continuation.coroutineContext,首先是要拿到 Continuation 物件的。但是,Kotlin 官方通過“suspend inline val coroutineContext”這個頂層變數,讓我們開發者能直接拿到 coroutineContext,卻對 Continuation 毫無感知。

掛起函數與 CoroutineContext 確實有著緊密的聯絡。每個掛起函數當中都會有 Continuation,而每個 Continuation 當中都會有 coroutineContext。並且,我們在掛起函數當中,就可以直接存取當前的 coroutineContext。

到此這篇關於Kotlin掛起函數應用介紹的文章就介紹到這了,更多相關Kotlin掛起函數內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


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