Kotlin挂起函数应用介绍

手机APP/开发
297
0
0
2023-06-21
标签   Kotlin
目录
  • 一、CPS转换
  • 1.CPS 参数变化
  • 2.CPS 返回值变化
  • 二、挂起函数的反编译
  • 三、Continuation

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

一、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(L)
    }
    return "UserInfo"
}
suspend fun getFriendListContent(user: String): String {
    withContext(Dispatchers.IO) {
        delay(L)
    }
    return "Friend, Friend2"
}
suspend fun getFeedListContent(user: String, list: String): String {
    withContext(Dispatchers.IO) {
        delay(L)
    }
    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 var) {
      Object $continuation;
      label: {
         if (var instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) !=) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label;
            }
         }
         $continuation = new ContinuationImpl(var) {
            // $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 var = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch(((<undefinedtype>)$continuation).label) {
      case:
         ResultKt.throwOnFailure($result);
         CoroutineContext var = (CoroutineContext)Dispatchers.getIO();
         Function var10001 = (Function2)(new Function2((Continuation)null) {
            int label;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               Object var = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               switch(this.label) {
               case:
                  ResultKt.throwOnFailure($result);
                  this.label =;
                  if (DelayKt.delay(L, this) == var2) {
                     return var;
                  }
                  break;
               case:
                  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");
               Function var3 = new <anonymous constructor>(completion);
               return var;
            }
            public final Object invoke(Object var, Object var2) {
               return ((<undefinedtype>)this.create(var, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
            }
         });
         ((<undefinedtype>)$continuation).label =;
         if (BuildersKt.withContext(var, var10001, (Continuation)$continuation) == var3) {
            return var;
         }
         break;
      case:
         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 var, @NotNull Continuation var1) {
      Object $continuation;
      label: {
         if (var instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) !=) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label;
            }
         }
         $continuation = new ContinuationImpl(var) {
            // $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 var = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch(((<undefinedtype>)$continuation).label) {
      case:
         ResultKt.throwOnFailure($result);
         CoroutineContext var = (CoroutineContext)Dispatchers.getIO();
         Function var10001 = (Function2)(new Function2((Continuation)null) {
            int label;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               Object var = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               switch(this.label) {
               case:
                  ResultKt.throwOnFailure($result);
                  this.label =;
                  if (DelayKt.delay(L, this) == var2) {
                     return var;
                  }
                  break;
               case:
                  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");
               Function var3 = new <anonymous constructor>(completion);
               return var;
            }
            public final Object invoke(Object var, Object var2) {
               return ((<undefinedtype>)this.create(var, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
            }
         });
         ((<undefinedtype>)$continuation).label =;
         if (BuildersKt.withContext(var, var10001, (Continuation)$continuation) == var4) {
            return var;
         }
         break;
      case:
         ResultKt.throwOnFailure($result);
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }
      return "Friend, Friend2";
   }
   @Nullable
   public static final Object getFeedListContent(@NotNull String var, @NotNull String var1, @NotNull Continuation var2) {
      Object $continuation;
      label: {
         if (var instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) !=) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label;
            }
         }
         $continuation = new ContinuationImpl(var) {
            // $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 var = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch(((<undefinedtype>)$continuation).label) {
      case:
         ResultKt.throwOnFailure($result);
         CoroutineContext var = (CoroutineContext)Dispatchers.getIO();
         Function var10001 = (Function2)(new Function2((Continuation)null) {
            int label;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               Object var = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               switch(this.label) {
               case:
                  ResultKt.throwOnFailure($result);
                  this.label =;
                  if (DelayKt.delay(L, this) == var2) {
                     return var;
                  }
                  break;
               case:
                  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");
               Function var3 = new <anonymous constructor>(completion);
               return var;
            }
            public final Object invoke(Object var, Object var2) {
               return ((<undefinedtype>)this.create(var, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
            }
         });
         ((<undefinedtype>)$continuation).label =;
         if (BuildersKt.withContext(var, var10001, (Continuation)$continuation) == var5) {
            return var;
         }
         break;
      case:
         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 var) {
      Object $continuation;
      label: {
         if (var instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) !=) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label;
            }
         }
         $continuation = new ContinuationImpl(var) {
            // $FF: synthetic field
            Object result;
            int label;
            Object L$;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return TestCoroutionKt.fetchContent(this);
            }
         };
      }
      Object var;
      label: {
         String userInfoContent;
         Object var;
         label: {
            Object $result = ((<undefinedtype>)$continuation).result;
            var = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch(((<undefinedtype>)$continuation).label) {
            case:
               ResultKt.throwOnFailure($result);
               ((<undefinedtype>)$continuation).label =;
               var = getUserInfoContent((Continuation)$continuation);
               if (var == var6) {
                  return var;
               }
               break;
            case:
               ResultKt.throwOnFailure($result);
               var = $result;
               break;
            case:
               userInfoContent = (String)((<undefinedtype>)$continuation).L$;
               ResultKt.throwOnFailure($result);
               var = $result;
               break label;
            case:
               ResultKt.throwOnFailure($result);
               var = $result;
               break label;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }
            userInfoContent = (String)var;
            ((<undefinedtype>)$continuation).L$ = userInfoContent;
            ((<undefinedtype>)$continuation).label =;
            var = getFriendListContent(userInfoContent, (Continuation)$continuation);
            if (var == var6) {
               return var;
            }
         }
         String friendListContent = (String)var;
         ((<undefinedtype>)$continuation).L$ = null;
         ((<undefinedtype>)$continuation).label =;
         var = getFeedListContent(userInfoContent, friendListContent, (Continuation)$continuation);
         if (var == var6) {
            return var;
         }
      }
      String var = (String)var10000;
      return Unit.INSTANCE;
   }
}

每一次函数调用的时候,continuation 都会作为最后一个参数传到挂起函数里,Kotlin 编译器帮我们做的,我们开发者是无感知。

2.CPS 返回值变化

final Object getUserInfoContent(@NotNull Continuation var)
final Object getFriendListContent(@NotNull String var, @NotNull Continuation var1)
final Object getFeedListContent(@NotNull String var, @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(L)
    }
    return "UserInfo"
}

执行到 withContext{} 的时候,就会返回 CoroutineSingletons.COROUTINE_SUSPENDED 表示函数被挂起了。

下面的函数则是伪挂起函数

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

因为它的方法体跟普通函数一样。它跟一般的挂起函数有个区别:在执行的时候,它并不会被挂起,因为它就是个普通函数。

二、挂起函数的反编译

 @Nullable
   public static final Object fetchContent(@NotNull Continuation var) {
      Object $continuation;
      label: {
         if (var instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) !=) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label;
            }
         }
         $continuation = new ContinuationImpl(var) {
            // $FF: synthetic field
            Object result;
            int label;
            Object L$;
            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return TestCoroutionKt.fetchContent(this);
            }
         };
      }
      Object var;
      label: {
         String userInfoContent;
         Object var;
         label: {
            Object $result = ((<undefinedtype>)$continuation).result;
            var = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch(((<undefinedtype>)$continuation).label) {
            case:
               ResultKt.throwOnFailure($result);
               ((<undefinedtype>)$continuation).label =;
               var = getUserInfoContent((Continuation)$continuation);
               if (var == var6) {
                  return var;
               }
               break;
            case:
               ResultKt.throwOnFailure($result);
               var = $result;
               break;
            case:
               userInfoContent = (String)((<undefinedtype>)$continuation).L$;
               ResultKt.throwOnFailure($result);
               var = $result;
               break label;
            case:
               ResultKt.throwOnFailure($result);
               var = $result;
               break label;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }
            userInfoContent = (String)var;
            ((<undefinedtype>)$continuation).L$ = userInfoContent;
            ((<undefinedtype>)$continuation).label =;
            var = getFriendListContent(userInfoContent, (Continuation)$continuation);
            if (var == var6) {
               return var;
            }
         }
         String friendListContent = (String)var;
         ((<undefinedtype>)$continuation).L$ = null;
         ((<undefinedtype>)$continuation).label =;
         var = getFeedListContent(userInfoContent, friendListContent, (Continuation)$continuation);
         if (var == var6) {
            return var;
         }
      }
      String var = (String)var10000;
      return Unit.INSTANCE;
   }

label 是用来代表协程状态机当中状态;

result 是用来存储当前挂起函数执行结果;

invokeSuspend 这个函数,是整个状态机的入口,它会将执行流程转交给 fetchContent 进行再次调用;

userInfoContent, friendListContent用来存储历史挂起函数执行结果。

if (var instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) !=) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label;
            }
         }
  $continuation = new ContinuationImpl(var) {
            // $FF: synthetic field
            Object result;
            int label;
            Object L$;
            @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 实例;

var = 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。