首页 > 其他分享 >launch原理解析

launch原理解析

时间:2023-06-22 14:05:44浏览次数:46  
标签:completion 协程 launch Continuation fun 原理 解析 public block


前言

本章就从Continuation入手来探究一下launch启动协程的原理。

正文

这里我们又回到了Continuation.kt这个文件,因为这是协程框架的基础元素,上一篇文章我们介绍了创建挂起函数的俩个高阶函数就是这个类中的基础层API。除此之外,在这个类,还有启动协程的基础API

协程启动的基础API

在前面文章我们说过,协程基础元素就像是砖头,其他中间层元素拿到这个砖头来构建一个房子,所以在Continuation.kt中也有2个启动协程的基础API:

//创建协程
public fun <R, T> (suspend R.() -> T).createCoroutine(
    receiver: R,
    completion: Continuation<T>
): Continuation<Unit> =
    SafeContinuation(createCoroutineUnintercepted(receiver, completion).intercepted(), COROUTINE_SUSPENDED)

//启动协程
public fun <T> (suspend () -> T).startCoroutine(
    completion: Continuation<T>
) {
    createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}

这里可以发现createCoroutine{}和startCoroutine{}都是扩展函数,而且扩展的接收者类型是(suspend () -> T),或许对Kotlin不熟悉的开发者对这个"给函数类型增加扩展"这种写法有点不适应,但是Kotlin中函数就是一等公民,普通类型可以有扩展,那函数类型自然也有。

那上面这个函数该如何使用呢,我们看一下下面代码:

fun main() {
    testStartCoroutine()
    Thread.sleep(2000L)
}

val block = suspend {
    println("Hello!")
    delay(1000L)
    println("World!")
    "Result"
}

private fun testStartCoroutine() {

    val continuation = object : Continuation<String> {
        override val context: CoroutineContext
            get() = EmptyCoroutineContext

        override fun resumeWith(result: Result<String>) {
            println("Result is: ${result.getOrNull()}")
        }
    }

    block.startCoroutine(continuation)
}
  • 这里定义了变量名为block的lambda表达式,它的类型是 suspend () -> String,其中lambda表达式最后一行表示返回类型。
  • 由于在block中调用了delay(1000)这个挂起函数,所以block也必须是挂起函数类型,这里也就是必须要添加suspend关键字的原因。
  • 定义了一个continuation变量,根据前一篇文章我们知道Continuation有2个作用:一种是在实现挂起函数的时候,用于传递挂起函数的执行结果;另一种是在调用挂起函数的时候,以匿名内部类的方式,接收挂起函数的执行结果。而上面代码的作用就是第二种,用来接收挂起函数或者叫做挂起block的lambda或者叫做协程的执行结果。

上面3点是代码的解读,根据之前实现挂起函数原理中的介绍,这里应该有一个Continuation实例对象,然后调用这个对象的resume方法,把结果返回,我们来看一下另一个底层API:createCoroutine{}或许就有了新的理解:

private fun testStartCoroutine() {

    val continuation = object : Continuation<String> {
        override val context: CoroutineContext
            get() = EmptyCoroutineContext

        override fun resumeWith(result: Result<String>) {
            println("Result is: ${result.getOrNull()}")
        }
    }

    val coroutine = block.createCoroutine(continuation)
    coroutine.resume(Unit)
}

可以发现我们这里返回值变量名就叫做coroutine,顾名思义就是一个协程,而这个协程的类型还是Continuation,但是这里并没有启动,只有调用了resume()方法才真正的启动协程。

所以底层基础创建协程API:createCoroutine{}和startCoroutine{}的差别也就是前者没有调用resume(),后者调用了resume()而已。

这里我们又可以猜测,根据挂起函数CPS和状态机原理**,调用continuation的resume方法会触发invokeSuspend()方法进入状态机**,我们下面就来看看startCoroutine()的原理。

startCoroutine{}原理解析

这里我们直接把上面代码进行反编译,可以得到如下代码:

public final class TestCoroutine1Kt {

 //注释1,main()函数
 public static final void main() {
      testStartCoroutine();
      Thread.sleep(2000L);
   }

   public static void main(String[] var0) {
      main();
   }

   //注释2,block变量
   @NotNull
   private static final Function1 block;
   
    @NotNull
   public static final Function1 getBlock() {
      return block;
   }

   //注释3,testStartCoroutine函数
   private static final void testStartCoroutine() {
      //匿名内部类实例,用于接收挂起函数的结果
      <undefinedtype> continuation = new Continuation() {
         @NotNull
         public CoroutineContext getContext() {
            return (CoroutineContext)EmptyCoroutineContext.INSTANCE;
         }

         public void resumeWith(@NotNull Object result) {
            String var2 = "Result is: " + (String)(Result.isFailure-impl(result) ? null : result);
            System.out.println(var2);
         }
      };
      //调用startCoroutine高阶函数
      ContinuationKt.startCoroutine(block, (Continuation)continuation);
   }

   //注释4,block对应的匿名内部类实例
   static {
      Function1 var0 = (Function1)(new Function1((Continuation)null) {
         int label;

         //注释5,进入状态机
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            String var2;
            switch(this.label) {
            case 0:
               ResultKt.throwOnFailure($result);
               var2 = "Hello!";
               System.out.println(var2);
               this.label = 1;
               if (DelayKt.delay(1000L, this) == var3) {
                  return var3;
               }
               break;
            case 1:
               ResultKt.throwOnFailure($result);
               break;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            var2 = "World!";
            System.out.println(var2);
            return "Result";
         }

         //注释6,创建匿名内部类实例
         public final Continuation create(@NotNull Continuation completion) {
            Intrinsics.checkNotNullParameter(completion, "completion");
            Function1 var2 = new <anonymous constructor>(completion);
            return var2;
         }

         //Function1接口的函数调用
         public final Object invoke(Object var1) {
            return ((<undefinedtype>)this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
         }
      });
      block = var0;
   }
}

上面反编译代码较多,我们来按照注释,仔细分析一下:

  1. 注释1是我们的main()函数,反编译后逻辑不会变化。
  2. 注释2是block变量,注意这里block在Kotlin代码中的类型是 suspend () -> String,然后根据CPS转换(这里也可以看成是一个挂起函数,毕竟和挂起函数类型是一样的,都是高阶函数类型)类型是(Continuation< String>) -> Any?,所以这里block的类型是Function1。
  3. 注释3就是testCoroutine()方法了,根据Kotlin的顶层函数语法,原来的block.startCoroutine(continuation)就变成了ContinuationKt.startCoroutine(block,continuaiton),这个函数调用原理后面细说。
  4. 注释4就是block具体实现类实例,首先我们可以看一下其中的方法:首先是注释5的invokeSuspend()函数,这个在之前说挂起函数原理时说过,这个是ContinuationImpl中定义的方法,而ContinuationImpl是Continuation的子类,所以其实现类既是Function1接口的实现类,也是Continuation子类。而这里的注释5,就是进入状态机,开始协程业务逻辑执行。
  5. 其次就是注释6的create方法,这里传入一个Continuation类型的completion实例,而且会调用返回整个匿名内部类实例,即这里会返回var0,即block实例。

上面第5点暂时不细说,我们先来看看testStartCoroutine()函数中的startCoroutine()方法:

public fun <T> (suspend () -> T).startCoroutine(
    completion: Continuation<T>
) {
    createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}

这里调用了createCoroutineUnintercepted()方法:

public expect fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
): Continuation<Unit>

会发现这里是用expect修饰的,即是一种声明,我们需要到协程源代码的JVM实现部分中找到对应的实现:

public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
): Continuation<Unit> {
    val probeCompletion = probeCoroutineCreated(completion)
    //注释2
    return if (this is BaseContinuationImpl)
        create(probeCompletion)
    else
        createCoroutineFromSuspendFunction(probeCompletion) {
            (this as Function1<Continuation<T>, Any?>).invoke(it)
        }
}

可以发现这也是(suspend () -> T)的扩展函数,所以this其实就是前面代码中的block,而前面说了反编译中block的实现类类型是继承至ContinuationImpl的,所以注释2的第一个if就能返回ture,而这里就是调用create(probeCompletion)函数。

而这个create()方法就是前面反编译中block实现类的create()方法:

@NotNull
public final Continuation create(@NotNull Continuation completion) {
   Intrinsics.checkNotNullParameter(completion, "completion");
   Function1 var2 = new <anonymous constructor>(completion);
   return var2;
}

注意了,这里返回值是Continuation类型对象,即调用完create()方法,其实就对应着协程被创建了,和挂起函数一样,类型是Continuation类型。

所以这里就好办了,根据前面的知识,这时调用resume,便会触发协程体的状态机入口,所以:

public fun <T> (suspend () -> T).startCoroutine(
    completion: Continuation<T>
) {
    createCoroutineUnintercepted(completion).intercepted().resume(Unit)
}

这里的最后调用就是resume(Unit),调用完resume就会调用continuation的invokeSuspend方法,从而开启协程的执行。

注意上面在resume()方法调用之前,还调用了intercepted()方法,我们简单看一下:

public expect fun <T> Continuation<T>.intercepted(): Continuation<T>

这个方法在Continuation.kt类中,是基础元素,同时也是用expect修饰的,所以我们要去Kotlin源码中找到JVM平台的实现:

public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
    (this as? ContinuationImpl)?.intercepted() ?: this

这里逻辑非常简单,就是将Continuation强转为ContinuationImpl,然后调用它的intercpeted()方法,而前面我们说过block实现类就是这个类的子类,所以强转一定能成功,而这个方法如下:

internal abstract class ContinuationImpl(
    completion: Continuation<Any?>?,
    private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {

    @Transient
    private var intercepted: Continuation<Any?>? = null

    public fun intercepted(): Continuation<Any?> =
        intercepted
            ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }
}

这里的逻辑其实就是通过ContinuationInterceptor类来对Continuation进行拦截和处理,而这里的处理其实就是将协程派发到线程上,这部分知识点等我们说Dispatchers时再细说。

所以到这里我们就大致说明白了底层启动协程API的原理,其中block就是一个协程,它的类型必须是suspend类型的,然后本质就是一个内部类,父类是Function1和Continuation,创建完协程就是返回一个内部类实例,而这个实例类型就是Continuation。

然后调用resume方法来触发Continuation进入其状态机

launch启动协程

其实有了startCoroutine{}这种基础API,中间层的launch、async、runBlocking这些API就是对基础底层API的封装,所以在理解完前面的原理后,这几个API就很好分析了。

我们还是先来看个例子:

fun main() {
    testLaunch()
    Thread.sleep(2000L)
}

private fun testLaunch() {
    val scope = CoroutineScope(Job())
    scope.launch {
        println("Hello!")
        delay(1000L)
        println("World!")
    }
}

这里我们创建了一个scope,然后使用这个scope开启了一个协程,我们直接反编译上面代码,看一下Java代码:

public final class TestCoroutine1Kt {
 
   //注释1 main()函数
   public static final void main() {
      testLaunch();
      Thread.sleep(2000L);
   }

   public static void main(String[] var0) {
      main();
   }

   private static final void testLaunch() {
      //创建scope
      CoroutineScope scope = CoroutineScopeKt.CoroutineScope((CoroutineContext)JobKt.Job$default((Job)null, 1, (Object)null));
      //注释2 开启协程
      BuildersKt.launch$default(scope, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
         int label;

         //状态机入口
         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            String var2;
            switch(this.label) {
            case 0:
               ResultKt.throwOnFailure($result);
               var2 = "Hello!";
               System.out.println(var2);
               this.label = 1;
               if (DelayKt.delay(1000L, this) == var3) {
                  return var3;
               }
               break;
            case 1:
               ResultKt.throwOnFailure($result);
               break;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            var2 = "World!";
            System.out.println(var2);
            return Unit.INSTANCE;
         }

        //熟悉的create方法
         @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);
         }
      }), 3, (Object)null);
   }
}

看了上面代码是不是有种熟悉的感觉,尤其是注释2中launch的第三个参数,这里就是和前面block是一样的,创建了一个匿名内部类,所以把上面代码写成下面这种:

private fun testLaunch() {
    val scope = CoroutineScope(Job())
    val block: suspend CoroutineScope.() -> Unit = {
        println("Hello!")
        delay(1000L)
        println("World!")
    }
    scope.launch(block = block)
}

这里的block就是前面基础API中的block类型,所以block就是协程,而本质上仍然是一个Continuation实例

其实到这里,我们就可以大概猜出,launch和底层API的区别了,不外乎就是加了一些启动配置,我们来看一下launch{}源码,来验证一下:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    //注释1 上下文
    val newContext = newCoroutineContext(context)
    //注释2 启动模式
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    //注释3 启动协程
    coroutine.start(start, coroutine, block)
    return coroutine
}
  • 注释1根据传入的CoroutineContext创建出新的Context,注意这里默认是EmptyCoroutineContext,而不是null;
  • 注释2是启动模式,也就是之前说的标准模式和懒加载模式
  • 注释3就是启动协程了,我们来跟进看一下。
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
    start(block, receiver, this)
}

这个方法是AbstractCoroutine这个类中的方法,而这个类很像Java中有Thread.java对应的代表线程的类,我们来看一下这个AbstractCoroutine类:

public abstract class AbstractCoroutine<in T>(
    parentContext: CoroutineContext,
    initParentJob: Boolean,
    active: Boolean
) : JobSupport(active), Job, Continuation<T>, CoroutineScope {

    init {
        if (initParentJob) initParentJob(parentContext[Job])
    }
    }

可以发现注释2返回的coroutine就是AbstractCoroutine的子类,我们继续跟进start方法,就会进入CoroutineStart.invoke()方法:

public enum class CoroutineStart {
    public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
        when (this) {
            DEFAULT -> block.startCoroutineCancellable(completion)
            ATOMIC -> block.startCoroutine(completion)
            UNDISPATCHED -> block.startCoroutineUndispatched(completion)
            LAZY -> Unit // will start lazily
        }
}

在这个invoke()方法中,会根据launch传入的启动模式,以不同的方式启动协程。

当我们启动模式是ATOMIC的时候,就会调用block.startCoroutine(completion),而这个就是前面所研究过的启动协程基础API,绕了一圈容易对接到了。

而对于其他俩个函数:startCoroutineCancellable(completion)和startCoroutineUndispatched(completion)只是在startCoroutine方法上加了一些额外功能,前者表示启动的协程可以响应取消,后者是不会被分发。

而对于默认情况下,我们不传入启动模式,就会执行默认的模式,也就是调用下面方法:

public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) {
    createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
}

然后下面这个方法也需要去对应的源码中找到:

public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
): Continuation<Unit> {
    val probeCompletion = probeCoroutineCreated(completion)

    return if (this is BaseContinuationImpl)
        create(probeCompletion)
    else
        createCoroutineFromSuspendFunction(probeCompletion) {
            (this as Function1<Continuation<T>, Any?>).invoke(it)
        }
}

这里就又回到了前面分析基础API的地方了,这里的create方法会调用block对应的匿名内部类的create方法,从而创建协程。

到这里,launch源码就分析完了,可以发现launch其实就是对底层启动协程的基础元素做了一层封装而已,可以让开发者选择上下文以及启动模式。

总结

本篇内容至关重要,我们从协程库底层的基础元素:startCoroutine{}和createCoroutine{}这2个高阶函数入手,可以发现 suspend () -> Unit 类型的block会被编译为FunctionN和Continuation的子类X(代号),当创建协程时,就是调用其create()方法创建X对象。

而启动协程则是利用Continuation的resume方法,来让X实例调用其invokeSuspend方法进入其内部状态机逻辑,开启协程逻辑。

最后就是launch只是对基础API的封装,让开发者可以灵活选择上下文以及启动模式。


标签:completion,协程,launch,Continuation,fun,原理,解析,public,block
From: https://blog.51cto.com/u_16163453/6534766

相关文章

  • 【flutter 起步走】flutter共享数据利器,InheritedWidget原理探秘
    知其然,也要知其所以然。最近的搬砖工作中,开发ui页面都是使用flutter,android原生只沦为了后台逻辑处理的后盾。在搬砖过程中,往往只要知道怎么用,便能搭起小房子,而要建的恢弘又大气,还是少不了对于原理的学习。在接触flutter中,Widget是我们接触最多的类。我们对于各种界面的搭建用的就......
  • postgresql SQL 优化 -- 理论与原理
    这里写的是一个系列,关于POSTGRESQLSQL优化的问题,这篇是这个系列的第二篇,第一篇可以在文字的末尾的连接中找到,之前有同学提出,希望有一个历史文字的连接。这期就进入正题,一个SQL语句撰写出来是怎么开始工作的,也就是查询的过程queryprocessing ,这里从几个步骤入手1  一个SQL......
  • 金九银十首战告捷,五年Android开发工程师面试经验分享(附面试题解析)
    笔者从前期准备到所有面试结束,花费了差不多3个月的时间。真可谓“面试造火箭,工作拧螺丝”,面试过程真的很累很辛苦。笔者面了很多公司,最终拿下了百度、腾讯和京东的offer,最后可能会选择京东。有人可能会问为什么不选择腾讯?的确腾讯的工资很高,福利待遇也很好。我觉得在京东能接触到更......
  • oracle 10053事件—执行计划的解析
    文档课题:oracle10053事件—执行计划的解析.数据库:oracle11.2.0.41、理论知识在查看SQL语句的执行计划时,CBO仅显示最终结果。当执行计划明显失真时,就急需知道CBO详细的选择过程。此时10053事件便闪亮登场,通过分析生成的trace文件揭开CBO的神秘外纱。2、数据准备2.1、建测试数......
  • Kotlin协程:Flow基础原理
    本文分析示例代码如下:launch(Dispatchers.Main){flow{emit(1)emit(2)}.collect{delay(1000)withContext(Dispatchers.IO){Log.d("liduo","$it")}Log.d("liduo",&......
  • 史上最全Android性能优化方案解析
    Android中的性能优分为以下几个方面:布局优化网络优化安装包优化内存优化卡顿优化启动优化……一.布局优化布局优化的本质就是减少View的层级。常见的布局优化方案如下:在LinearLayout和RelativeLayout都可以完成布局的情况下优先选择LinearLayout,可以减少View的层级,但是注意相同组......
  • 字节跳动总监封神之作《Android11.0最新Framework解析》,1595页,限时免费下载高清PDF文
    Framework始终穿插在App整个研发生命周期中,不管是从0到1的建立阶段,还是从1到N打磨阶段,都离不开Framework。成为一名AndroidFramework高手,就会成为招聘中非常稀缺的人才,可以成为你的敲门砖。很多同学都表示在面试时必问Framework相关问题。因为目前大公司的app开发都要基......
  • 字节总监用了半个月整理出的1595页《Android11.0 最新Framework解析》高清PDF开发下载
    作为过来人,发现很多学习者和实践者都在AndroidFramework上面临着很多的困扰,比如:工作场景中遇到难题,往往只能靠盲猜和感觉,用临时性的补救措施去掩盖,看似解决了问题,但下次同样的问题又会发作,原因则是缺乏方法论、思路的指引以及工具支持;能力修炼中,缺乏互联网项目这一实践环境,对Fram......
  • 牛掰,阿里P7程序员花了半个月,编成这份1880页的《Android百大框架源码解析》,快来收藏
    为什么要深入了解源码?只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是浮于表象,这对我们的知识体系的建立和完备以及实战技术的提升都是不利的。真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读Android系统源码,还包括各种优秀的开源库。一方面,这些作品都......
  • 一文全解析KMP算法
    假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?如果用暴力匹配的思路,并假设现在文本串S匹配到i位置,模式串P匹配到j位置,则有:如果当前字符匹配成功(即S[i]==P[j]),则i++,j++,继续匹配下一个字符;如果失配(即S[i]!=P[j]),令i=i-(j......