Princípio da função de suspensão Kotlin

Geralmente leio blogs ou aprendo conhecimento, e as coisas que aprendo são relativamente dispersas, não existe um conceito de módulo de conhecimento independente e é fácil esquecer depois de aprender. Então eu montei meu próprio repositório de notas (um repositório de notas que eu mantenho por um longo tempo. Se você estiver interessado, você pode clicar em uma estrela~ Sua estrela é uma grande força motriz para minha escrita), classificar tudo que eu costumo aprender, e em seguida, coloque-o nele, e é fácil revisar quando você precisar.

1. Prefácio

As funções de suspensão do Kotlin são frequentemente usadas no aprendizado e no trabalho, e é necessário dominar seus princípios. Este artigo irá levá-lo passo a passo para analisar sua implementação de princípio.

ps: A versão Kotlin usada neste artigo é 1.7.0.

2. Princípio da CPS

Adicione uma função de suspensão na frente de uma função Kotlin e ela se torna uma função de suspensão (embora possa não ser suspensa internamente, e aquela que não é suspensa internamente é chamada de função de pseudo-suspensão).

Escreva uma função de suspensão primeiro

suspend fun getUserName(): String {
    delay(1000L)
    return "云天明"
}

Em seguida, através de Tools->Kotlin->Show Kotlin Bytecode->Decompile do Android Studio, agora obtemos o código Java após a descompilação do bytecode Kotlin:

public static final Object getUserName(@NotNull Continuation var0) {
    ...
}

Você pode ver que depois que a função é compilada, há um parâmetro Continuation adicional e, em segundo lugar, o valor de retorno se torna Object. Abaixo, discutimos essas duas mudanças em detalhes: parâmetros de função e valores de retorno de função.

Alterações nos parâmetros do CPS

A suspend fun getUserName(): Stringfunção acima, se eu a chamar em Java, verei o Android Studio nos solicitar

Java中看到的suspend函数

Como você pode ver na figura, um novo parâmetro, Continuation, é na verdade um Callback, mas o nome é alterado.

Veja sua definição:

/**
 * 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>)
}

Essa interface de retorno de chamada enviará o resultado do retorno de chamada resumeWith para o exterior.

Alterações no valor de retorno do CPS

Na definição da interface de Continuação acima, na verdade há um pequeno detalhe, é preciso um T genérico. Esse tipo genérico T é o tipo do valor de retorno de nossa função de suspensão. O valor de retorno de getUserName acima é String. Após a compilação, essa String chega ao tipo genérico de Continuação.

而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
}

Como a função showMoments tem a palavra-chave suspend, ela eventualmente passará por conversão de CPS e terá um parâmetro Continuation. Ao chamar showMoments em Java, você deve passar a Continuação. Continuation é uma interface, você precisa passar uma classe de implementação para implementar getContext e 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);
    }
});

Chamar uma função de suspensão em Java se parece com chamar um método. Esse método precisa passar um retorno de chamada. O valor de retorno desse método é fornecido pelo retorno de chamada e você pode personalizar em qual encadeamento o método é executado.

7. Resumo

Bem, a função de suspensão do Kotlin de hoje é analisada aqui, basicamente o mistério foi resolvido (exceto quando o invokeSuspend é chamado de volta, e compartilharei com você quando tiver a oportunidade).

A função de suspensão do Kotlin é essencialmente: CPS + máquina de estado.

  • CPS: As funções suspensas têm mais palavras-chave de suspensão do que as funções comuns, e o compilador Kotlin as tratará especialmente. Converta a função em uma função com Callback, Callback é a interface de Continuação e seu tipo genérico é o tipo de valor de retorno da função original. O tipo de valor de retorno após a conversão é Any?, pois a palavra-chave suspend não é necessariamente suspensa, se for suspensa, retornará Intrinsics.COROUTINE_SUSPENDED, e a função pseudo-suspender (não há outras funções de suspensão nela, mas a conversão CPS ainda será executada) é retornado diretamente. Como resultado, esse resultado pode ser de qualquer tipo, portanto, o valor de retorno só pode ser Any?.
  • Máquina de estado: Quando a função de suspensão for compilada, ela se tornará uma estrutura de máquina de estado composta de switch e label. O rótulo representa o estado específico da máquina de estado atual, cada vez que muda, significa que a função de suspensão é chamada uma vez. Uma interface de Callback será criada dentro dela. Quando suspensa, o resultado da função suspensa será retornado através de Callback. Após o callback, como o rótulo já foi modificado antes, de acordo com o rótulo, julga-se que ele deve continuar inativo e execute a lógica back. O retorno de chamada acima é Continuação, acho que seu significado aqui pode ser traduzido para continuar a executar o código restante.

Acho que você gosta

Origin juejin.im/post/7118508111735816199
Recomendado
Clasificación