Kotlin挂起函数基础

Posted 且听真言

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin挂起函数基础相关的知识,希望对你有一定的参考价值。

学习了极客时间课程,记录下学习输出。 

  一、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挂起函数整理-基础

Kotlin 协程协程的挂起和恢复 ① ( 协程的挂起和恢复概念 | 协程的 suspend 挂起函数 )

Kotlin 协程协程的挂起和恢复 ① ( 协程的挂起和恢复概念 | 协程的 suspend 挂起函数 )

Kotlin 协程中的挂起函数是啥意思?

Kotlin 挂起函数在调用 java 时丢失 ReactiveSecurityContext

Kotlin 协程不会立即编译(挂起函数?)