kotlin语法进阶 - 协程(二)挂起函数原理

网上有许多挂起函数的原理,但都讲的太过深入或者示例太复杂,对新手很不友好,作为一个有理想的菜鸟,我将用最简单的示例让大家用几分钟的时间理解挂起函数的原理。

被suspend关键字修饰的函数被称为挂起函数,字面意思就是可以被挂起的函数,挂起可以理解为暂停的意思,能暂停就能恢复。
那挂起函数是怎么实现代码的暂停和恢复的呢?

如下:我们随便定义一个挂起函数,然后使用Android Studio 将kotlin反编译成Java代码。(反编译步骤:Tools->Kotlin->Show Kotlin Bytecode->Decompile)

suspend fun getInfo() : String {
    
    
    delay(1000L)
    Log.e("---" , "延迟后执行的代码")
    return "hello world!"
}

反编译后如下:暂时先省略方法里面的代码

public final Object getInfo(@NotNull Continuation var1) {
    
    ...
}

可以看到,反编译后的代码 多了一个Continuation参数,并且返回值变成了Object,我们查看一下Continuation,如下是一个接口,有一个CoroutineContext的接口属性和一个resumeWith的接口方法。

public interface Continuation<in T> {
    
    
/**
 * The context of the coroutine that corresponds to this continuation.
 */

    public val context: CoroutineContext
/**
 * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
 * return value of the last suspension point.
 */

    public fun resumeWith(result: Result<T>)
}

翻译备注可以知道,resumeWith的作用是:继续执行相应的协程,将成功或失败的结果传递为最后一个暂停点的返回值。
所以:Continuation 其实就是一个带有泛型参数的 CallBack回调接口。这个从挂起函数转换成CallBack 函数的过程,被称为:CPS 转换(Continuation-Passing-Style Transformation)

那么这个回调接口有什么作用呢?
我们查看完整的反编译代码:

@Nullable
public final Object getInfo(@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 ThirdActivity.this.getInfo(this);
         }
      };
   }

   Object $result = ((<undefinedtype>)$continuation).result;
   Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
   switch(((<undefinedtype>)$continuation).label) {
    
    
   case 0:
      ResultKt.throwOnFailure($result);
      ((<undefinedtype>)$continuation).label = 1;
      if (DelayKt.delay(1000L, (Continuation)$continuation) == var4) {
    
    
         return var4;
      }
      break;
   case 1:
      ResultKt.throwOnFailure($result);
      break;
   default:
      throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
   }

   Log.e("---", "延迟后执行的代码");
   return "hello world!";
}

代码看着很乱,我们一步步分析:

  1. getInfo方法接收一个Continuation对象,已知这是一个回调接口,用于协程恢复的时候调用
  2. 将Continuation封装成了一个ContinuationImpl对象$continuation
    ,该对象还有一个标志位lable,初始等于0,并且有一个invokeSuspend()方法,该方法会调用getInfo方法,嗯?又调用回来了,看来这就是用来恢复代码用的!
  3. 下面的代码竟然是一个switch case, 我们单独看一下这个switch case如下,他按照$continuation的lable值把代码分成了两部分,状态 =0 的时候,执行了挂起函数delay并且把 $continuation传给了delay方法,还把lable变成了1,然后直接return返回不再执行后面的代码了。那么,后面会发生什么呢,当delay方法运行完成以后,会调用 $continuation的invokeSuspend()方法,该方法又会调用getInfo方法恢复代码,但这时候 $continuation的lable值已经是1了,switch case会进入1的判断,继续执行后面的代码。
    那么,整体流程就非常清晰了,我们通过状态机将代码分成了两部分(挂起前和挂起后),然后通过回调接口在挂起函数执行完毕以后回来再次执行原函数,但因为状态机的状态已经变了,我们只能执行挂起后的代码。

所以:挂起函数的本质就是 CallBack回调接口 + 状态机(switch case + 状态值 实现 代码分段执行)

我们再想一下,我们都把代码分段了,我们在代码分段的基础上增加一下线程切换,这不过分吧。
我们再想一下,挂起函数用的是回调接口,我们在回调接口的基础上增加嵌套或者并行,这不过分吧。
这思路、代码功能的实现不就扩展起来了吗!大家可以查看反编译更多kotlin协程提供给我们的方法,去验证各种思路。

可以看到,kotlin协程底层为我们做了非常多的事情,使我们开发者用最简单的方法实现复杂的功能,这也是kotlin协程的优势。

本文主要用来快速了解挂起函数的原理,肯定有讲的不合适的地方,希望大家多多指正。

猜你喜欢

转载自blog.csdn.net/weixin_43864176/article/details/126373256