Kotlin是基于JVM的一个言语,也是很时尚的言语。Java言语这几年的开展,借鉴了Kotlin的许多特性。Google把Kotlin作为Android的优先运用言语之后,更是应者影从。本文整理了在Kotlin学习和运用中总结整理出来的几个有意思的知识点,和咱们学习和沟通。

Coroutines ARE light-weight

fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}

以上代码在学习Kotlin协程的时分应该都见过了,是为了阐明协程很轻量。原因也很简略,在一般的操作体系中,用户线程和内核线程是1对1的联系,操效果户线程的便是操作内核线程,每一个内核线程,都有专门的内核数据结构来办理,在Linux里边运用数据结构task_struct来办理的,这是一个很杂乱的数据结构,内核线程的挂起和履行,都会涉及到变量和寄存器数据的保存和康复,乃至内核线程自身的调度就需求耗费CPU的时间。可是协程完全是用户空间里边的工作,说的简略点,便是几个使命的履行队列的办理,当然协程也是运转中线程之上的。

有个疑问产生了?那为什么现在操作体系用户线程和内核线程是1对1的联系呢?

由于在早期的Java版别中,在单核CPU的时代,用户线程和内核线程的联系是多对一。在多核时代,也有多对多的模型。即多路复用多个用户级线程到同样数量或者更少数量的内核线程。在Solaris早几年的版别也是支撑类似的多对多的模型,咱们想过没有,为什么现在简直一切的操作体系都运用1对1的模型了呢?

以下是一家之言,和咱们探讨。OS自身越来越杂乱,参与方也越来越多。之前线程这块分两层,内核层和用户线程库。用户线程库为程序员提供创立和办理线程的API。随着互联网的开展,有一些需求产生了,如高并发支撑,OS这一层比较粗笨的,很难快速满意越来越快的需求的改变。这个时分,一些言语,在规划之初就考虑来处理这些新产生的问题,一些时尚的言语,也十分快速的来呼应这些需求。所以就有了在线程库之上,在言语的层面来处理这些问题,所以协程产生了,并且越来越多的言语支撑了这些特性。

哈哈,为什么线程库为什么没有演进来支撑协程呢?原因也很简略,线程库根本被定位成办理内核线程的接口,并且线程库的作者的首要精力也不在这个方向。线程库做好自己的工作(办理内核线程),然后把其他的交给他人。这也是自然形成的分工和分层。

想想这几年的Android运用开发的开展,AndroidX里的东西越来越多,演进也越来越快。这是由于Android体系的体量约束,不或许跑地很快,一年一次算得上是OS升级的极限了。所以必须把需求跑得快的东西剥离出来。这个道理和协程的开展也有殊途同归之处。

Lambda表达式捕获变量

Lambda表达式应该是一个历史比较悠长的东西了,由于函数式编程流行,Lambda表达式也是被十分广泛地运用。Java对Lambda的支撑比较后知后觉,应该是在Java8才开端支撑的吧,不过在JDK7的时分,JVM字节码引进了InvokeDynamic,后续应该会成为各个基于JVM言语解析Lambda表达式的一致的规范办法。后边会有独自一段来讨论这个InvokeDynamic。

Lambda本质上是一个函数,一块能够被履行的代码段。在函数式编程环境下,Lambda表达式能够被传递,保存和回来。在类似的C/C++的言语环境中,函数指针应该能够十分便利和高效的来完结Lambda,可是在Java和Kotlin这样的言语中,没有函数指针这样的东西,所以用目标来存储Lambda,这当然是在没有InvokeDynamic之前(Java7)。

提到Lambda表达式,咱们还记不记得在Java中,假如Lambda要引进外部的一些变量时,这个变量必定要被声明为final。

public Runnable test() {
    int i = 1000;
    Runnable r = () -> System.out.println(i);
    return r;
}

上面这段代码,变量i为实际上的final,编译器会把这个i变量自动加上final。

如下的代码端编译就会出错了,由于变量i不是final的。

public Runnable test() {
    int i = 1000;
    i++;
    // Variable used in lambda expression should be final or effectively final
    Runnable r = () -> System.out.println(i);
    return r;
}

会什么会有这个约束呢?呵呵,你看上面test函数,假如调用test函数,然后把回来的目标保存下来后再履行,这个时分i这个变量现已在内存中销毁掉了,这个lambda也就无法履行了。由于i这个变量是栈变量,生命周期只在test函数履行期间存在。那么为什么声明称final就没工作了呢,由于在这种情况下,变量是final的,lambda能够把这个变量Copy过来。换句话说,lambda履行的是这个变量的Copy,而不是原始值。

讲到这儿,假如你熟悉Kotlin的话,你知道Kotlin没有这个约束,引证的变量不是非得被声明为final啊。难道Kotlin就没有Java遇到的问题吗?

Kotlin相同会遇到同样的问题,仅仅Kotlin的编译器比较聪明能干啊,它把Lambda引证到的变量都变成final了啊。哈哈,或许你发现了,假如变量自身不是final的,强制变成final这就不会有问题吗?

fun test(): () -> Unit {
    var i = 0
    i++
    return {
        println("i = $i")
    }
}

以上的代码是能够正常编译被履行的。原因便是编译器干了一些工作,它把i变成堆变量(IntRef)了,并且声明晰一个final的栈变量来指向堆变量。

public static final Function0 test() {
   final IntRef i = new IntRef();
   i.element = 0;
   int var10001 = i.element++;
   return (Function0)(new Function0() {
      // $FF: synthetic method
      // $FF: bridge method
      public Object invoke() {
         this.invoke();
         return Unit.INSTANCE;
      }
      public final void invoke() {
         String var1 = "i = " + i.element;
         System.out.println(var1);
      }
   });
}

呵呵,其实吧,这是违背函数式编程的准则的。函数只需求依靠函数的输入,假如引证外部变量,会导致函数输出的不确认性。或许会导致一些偶现的很难处理的bug。 尤其假如在函数里边修改这些变量的话,如在final的List目标里边进行add/remove,这样还会有并发的安全隐患。

Invokedynamic防止为Lambda创立匿名目标

先稍微介绍一下字节码InvokeDynamic指令,需求更详细能够检查官方文档。这个指令最开端是在JDK7引进的,为了支撑运转在JVM上面的动态类型言语。

先看如下代码,一个简略的Lambda表达式。

public Consumer<Integer> test() {
    Consumer<Integer> r = (Integer i) -> {
        StringBuilder sb = new StringBuilder();
        sb.append("hello world").append(i);
        System.out.println(sb.toString());
    };
    return r;
}

检查编译之后的字节码如下:

  public java.util.function.Consumer<java.lang.Integer> test();
    descriptor: ()Ljava/util/function/Consumer;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=1
         0: invokedynamic #2,  0              // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
         5: astore_1
         6: aload_1
         7: areturn
      LineNumberTable:
        line 7: 0
        line 12: 6
    Signature: #20                          // ()Ljava/util/function/Consumer<Ljava/lang/Integer;>;

能够看到,详细Lambda表达式被invokedynamic替代,能够将完结Lambda表达式的这部分的字节码生成推迟的运转时。这样防止了匿名目标的创立,并且没有额外的开销,由于原本也是从Java字节码进行函数目标的创立。并且假如这个Lambda没有被运用到的话,这个过程也不会被创立。假如这个Lambda被调用屡次的话,只会在榜首次进行这样的转化,其后一切的Lambda调用直接调用之前的链接的完结。

Kotlin由于需求兼容Java6,无法运用invokedynamic,所以编译器会为每个Lambda生成一个.class文件。这些文件通常为XXX$1的办法出现。生成很多的类文件是对性能有负面影响的,由于每个类文件在运用之前都要进行加载和验证,这会影呼运用的启动时间,尤其是Lambda被很多运用之后。不过尽管Kotlin现在不支撑,可是应该会在不久的将来就支撑了。

能够在一些适宜的场景下,运用inline来防止匿名目标的创立,Kotlin内置的许多办法都是inline的。也要留意,假如inline要害字运用不当,也会形成字节码胀大,并影响性能。

Callback转协程

现在许多库函数都运用回调来进行异步处理,可是回调会有一些问题。首要有两方面吧

  1. 错误处理比较麻烦。
  2. 在一些循环中处理回调也会是麻烦的工作。

所以假如咱们在工程中遇到回调API的话,一般的做法会把这些回调转化成协程,这样就能够用协程进行一致处理了。 回调大致分两类:

  1. 一次性工作回调,用suspendCancellableCoroutine处理。
  2. 屡次工作回调,用callbackFlow处理。

咱们先来看下用suspendCancellableCoroutine,以下是模板代码,运用这段模板代码能够便利的把恣意回调便利地转化成协程。

suspend fun awaitCallback(): T = suspendCancellableCoroutine { continuation ->
    val callback = object : Callback { // Implementation of some callback interface
        override fun onCompleted(value: T) {
            // Resume coroutine with a value provided by the callback
            continuation.resume(value)
        }
        override fun onApiError(cause: Throwable) {
            // Resume coroutine with an exception provided by the callback
            continuation.resumeWithException(cause)
        }
    }
    // Register callback with an API
    api.register(callback)
    // Remove callback on cancellation
    continuation.invokeOnCancellation { api.unregister(callback) }
    // At this point the coroutine is suspended by suspendCancellableCoroutine until callback fires
}

接下来,咱们来看看suspendCancellableCoroutine这个函数到底干了什么,在注释里边有相关的代码的解释。

public suspend inline fun <T> suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation<T>) -> Unit
): T =
    suspendCoroutineUninterceptedOrReturn { uCont ->
        // 从最开端调用suspend函数的当地获取Continuation目标,并对把目标转化成CancellableContinuation目标。
        val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
        cancellable.initCancellability()
        // 调用block进行回调的注册
        block(cancellable)
        // 这个函数有个逻辑,假如回调现已完毕,直接回来,调用者不进行挂起
        // 假如回调还没有完毕,回来COROUTINE_SUSPENDED,调用者挂起。
        cancellable.getResult()
    }

这儿有一个要害的函数suspendCoroutineUninterceptedOrReturn,榜首次看到这个函数的时分,就感到困惑,uCont这个变量是从哪里来的,这个当地光看代码是看不出从哪里来的。原因是这个uCont变量最终是编译器来处理的。每个suspend函数在编译的时分都会在参数列表最终增加一个Continuation的变量,在调用suspendCoroutineUninterceptedOrReturn的时分,会把调用者的Continuation的目标赋值给uCont。

一切这个函数给了咱们一个时机,手动来处理suspend要害字给咱们增加的那个参数目标。为什么咱们要手动来处理呢,由于咱们要把Continuation的目标转化成CancellableContinuation目标,这样咱们就能够在被撤销的时分来把回调给撤销掉了。

假如要完全看懂以上代码,需求知道suspend要害字后边的逻辑,后边会有专门一节来阐明。

关于屡次工作的回调处理callbackFlow,根本逻辑与要害知识点和上面说的一致,所以这儿不对callbackFlow进行阐明晰。

在suspend要害字后边

Kotlin协程是一个用户空间(相关于内核空间)完结的异步编程的结构。suspend要害字的处理是其间比较要害的一部分,要了解Kotlin的协程如何完结挂起和康复,就必须要了解suspend要害字的后边的故事。

在讲suspend之前,咱们先来了解一下Continuation-passing style(CPS)。 先来一道开胃菜,已知直角三角形的两条直角边长度分别为a和b,求斜边的长度 ?

define (a, b) {
    return sqrt(a * a + b * b)
}

用勾股定理,能够用上面的代码能够轻松处理。上面的写法是典型的指令式编程,通过指令的组合办法处理问题。

现在咱们来看看CPS的计划的代码应该如何来写?

define (a, b, continuation: (x, y) -> sqrt(x + y)) {
    return continuation(a * a, b * b)
}

这儿的CPS写法,把勾股定理分红两部分,榜首部分计算直角边的平方和,第二部分进行开方操作。开方作为函数的参数传入,当榜首部分计算完结之后,进行第二部分的操作。

哈哈,这不便是Callback的吗?没错CPS的本质便是Callback,或者说CPS便是通过Callback来完结的。当然假如仅仅把CPS了解Callback也是不完全精确。CPS要求每个函数,都需求指定这个函数履行完结之后的接下来的操作,所以这个名词应该是continuation,而不是callback,一般情况下,代码里边不会回来callback的履行成果,由于callback的语义上不是持续要干的工作,Continuation才是持续要干的工作,然后把最终的成果回来。

Kotlin编译器便是把suspend函数变成CPS的函数,来完结函数的挂起和康复的。咱们先来看最简略的比如,这个函数没有参数,也没有回来。先打印hello,1s之后再打印world。

suspend fun test() {
    println("hello")
    delay(1000L)
    println("world")
}

关于这个函数,编译器做了两个首要的工作:

  1. 通过状况机的机制,把这个函数逻辑上分为两个函数,榜首个函数是榜首次被调用的,另一个函数是在这个函数从挂起状况康复的时分调用,也便是在delay 1s之后履行的。
  2. 给suspend函数的参数增加continuation的参数,并在函数体里边对这个参数进行处理。 下面来看下,这个函数再被编译器处理之后的代码,代码以伪代码的办法给出。
fun test(con: Continuation): Any? {
    class MyContinuation(
        continuation: Continuation<*>): ContinuationImpl(continuation) {
        var result: Any? = null
        var label = 0
        override fun invokeSuspend(res: Any?): Any? {
            this.result = res
            return test(this);
        }
    }
    val continuation = con as? MyContinuation
        ?: MyContinuation(con)
    if (continuation.label == 0) {
        println("hello")
        continuation.label = 1
        if (delay(1000, continuation) == COROUTINE_SUSPENDED) {
            return COROUTINE_SUSPENDED
        }
    }
    if (continuation.label == 1) {
        println("world")
        return Unit
    }
    error("error")
}

每一个suspend函数,都有一个continuation参数传入,自己也有一个continuation,包括传入的continuation,自己的continuation目标的类是自己独有的,一般会是一个匿名内部类,这儿为了好了解,我把这个匿名的内部来变成普通的类,便于阐明问题。在榜首次调用这个函数的时分,会实例化自己的continuation目标。实例化的逻辑是

val continuation = con as? MyContinuation
    ?: MyContinuation(con)

这儿有个特别要害的MyContinuation目标的变量label,初始化为0,所以函数榜首次履行的是代码里边label等于0的分支,通过这样状况机的机制,把函数从逻辑上能够分红多个函数。

再来看下上面函数体里边的COROUTINE_SUSPENDED,当delay函数回来COROUTINE_SUSPENDED,这个函数也回来COROUTINE_SUSPENDED,同样,假如有函数调用这个函数的时分,也回来COROUTINE_SUSPENDED。这个标识便是用来指示函数进入了挂起状况,等着被回调了。所以函数挂起的本质是,这个函数在当时的label分支下回来了

假如suspend函数没有回来COROUTINE_SUSPENDED呢,那就接着履行,履行函数下一个状况的逻辑。所以函数在进入当时的状况的时分,就要马上把下个状况设置好。 continuation.label = 1。假如当时函数进入挂起状况,就会把当时的continuation目标传入到调用的函数中,当函数需求康复的时分,会调用continuation的invokeSuspend的办法,就会从头履行这个函数,这儿便是一个Callback了。当然会进入label等于1的分支。所以函数康复的本质是,这个函数在新Label状况下被从头调用了。

留意了suspend函数不必定回来COROUTINE_SUSPENDED的,也或许回来详细的值。如以下的函数:

suspend fun test(): String {
    return "hello"
}

这个函数就没必要进入挂起了,没有回来COROUTINE_SUSPENDED,在这种情况下,函数会履行下一个label分支。 这也是为什么每个suspend函数在编译器处理之后的函数回来值是Any?。这其实是一个union的结构体,仅仅现在Kotlin还不支撑union这样的概念,不过Kotlin改变这么快,之后没准也会支撑。

这儿的MyContinuation承继了ContinuationImpl,所以看起来MyContinuation完结的比较简略,由于许多的杂乱的逻辑都封装在ContinuationImpl中了。下面咱们尝试用一个更杂乱的比如,然后自己完结ContinuationImpl,更完整来看下背面的逻辑。

鄙人面的比如中,suspend会更杂乱,有参数,有回来。

suspend fun test(token: String) {
    println("hello")
    val userId = getUserId(token) // suspending
    println("userId: $userId")
    val userName = getUserName(userId) // suspending
    println("id: $userId, name: $userName")
    println("world")
}

编译器处理过的代码大致如下:

fun test(
    token: String,
    con: Continuation
): Any? {
    val continuation = con as? MyContinuation
        ?: MyContinuation(con)
    var result: Result<Any>? = continuation.result
    var userId: String? = continuation.userId
    val userName: String
    if (continuation.label == 0) {
        println("hello")
        continuation.label = 1
        val res = getUserId(token, continuation)
        if (res == COROUTINE_SUSPENDED) {
            return COROUTINE_SUSPENDED
        }
        result = Result.success(res)
    }
    if (continuation.label == 1) {
        userId = result.getOrThrow() as String
        println("userId: $userId")
        continuation.label = 2
        continuation.userId = userId
        val res = getUserName(userId, continuation)
        if (res == COROUTINE_SUSPENDED) {
            return COROUTINE_SUSPENDED
        }
        result = Result.success(res)
    }
    if (continuation.label == 2) {
        userName = result.getOrThrow() as String
        println("id: $userId, name: $userName")
        println("world")
        return Unit
    }
    error("error")
}

MyContinuation的代码如下:

class MyContinuation(
    val completion: Continuation<Unit>,
    val token: String
) : Continuation<String> {
    override val context: CoroutineContext
        get() = completion.context
    var label = 0
    var result: Result<Any>? = null
    var userId: String? = null
    override fun resumeWith(result: Result<String>) {
        this.result = result
        val res = try {
            val r = test(token, this)
            if (r == COROUTINE_SUSPENDED) return
            Result.success(r as Unit)
        } catch (e: Throwable) {
            Result.failure(e)
        }
        completion.resumeWith(res)
    }
}

还记得函数调用,一般都是通过Stack来处理,局部变量和函数的回来之后持续履行的地址都存在stack frame中,这儿Continuation的效果就相当于这个Stack了。

还有一个小小的知识点,suspend函数能够调用suspend,所以总有一个最初始suspend函数的吧,不会不就没止境了啊。最初始的那个suspend函数必定是从Callback转化而来的,这儿详细能够检查上一节关于Callback转suspend函数的介绍。

以上较多参阅了Coroutines under the hood这篇文章,并参加了一些自己的考虑,许多都是自己的了解,肯定有错误和不足之处,也请指正。

Corountine Job.join()的一个大坑

这问题起源于我的别的一篇文章,运用程序启动优化新思路 – Kotlin协程,文章讲的是运用启动时,通过Kotlin协程的计划,简化多使命并行初始化的代码逻辑。其实这类问题具有普遍性,我现在举别的一个比如来阐明。

做过Android体系开发的工程师必定知道,编译整个Android体系是耗时的,由于里边有至少有数百个模块,模块和模块之间也或许存在依靠联系。这儿一般体系都是支撑多线程并行编译的,那如何来运用多线程来组织这些模块的编译呢?

考虑一个最简略的比如,现在有5个build tasks,依靠联系如下:

Kotlin知识点的深入思考

这个图是一个典型的有向无环图,按照拓扑排序的顺利来履行即可,下面考虑运用协程来多使命并行。首要,使命1和使命2没有被依靠,能够被启动,这儿能够并行的履行。

suspend fun build() = coroutineScope {
    // 调度协程
    val job1 = launch(Dispatchers.Default) { 
        // start build task 1.
    }
    val job2 = launch(Dispatchers.Default) { 
        // start build task 2.
    }
}

接下来有些棘手,使命3,使命4,使命5都是有依靠的,可是咱们无法知道使命1和使命2什么时分能够履行完结,所以咱们运用了Kotlin协程体系的Join来进行等候。可是这儿要留意,咱们不能在调度协程里边进行对Job的Join操作。如以下的代码就会存在问题:

suspend fun build() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        // start build task 1.
    }
    val job2 = launch(Dispatchers.Default) {
        // start build task 2.
    }
    job1.join()
    val job3 = launch(Dispatchers.Default) { 
        // start build task  3.
    }
}

假如Task1很耗时,Task3需求等Task1完结之后履行,可是Task2很快就履行完了,能够组织Task5进行履行,假如在调度协程中进行Join,就会一直处于等候Task1履行完结,所以Join的等候不能在调度协程中,那怎么办呢? 咱们能够在Task使命协程中进行等候,就能够处理这个问题了。如下面的代码。

suspend fun build() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        // start build task 1.
    }
    val job2 = launch(Dispatchers.Default) {
        // start build task 2.
    }
    val job3 = launch(Dispatchers.Default) {
        job1.join()    
        // start build task  3.
    }
    val job4 = launch(Dispatchers.Default) {
        job1.join()
        job2.join()
        // start build task 4.
    }
    val job5 = launch(Dispatchers.Default) { 
        job2.join()
        // start build task 5.
    }
}

以上代码运转良好,咱们在测试的时分,一切的逻辑都按照咱们所料想的办法履行。可是有一天,咱们发现,有十分低的概率发生,这些使命会无法完毕,也便是以上的build办法没办法回来,这是一个概率极低的工作,可是的确存在,哈哈,咱们掉坑里去了。所以咱们就去看了Join的源码,想看看到底发生了什么工作?首要,假如你看懂了上面关于Suspend的那节的话,你会清楚的知道Join是如何进行挂起的,从头康复必然会走Continuation的resume办法。

以上是咱们的大致的主意,然后咱们来看一下Join函数到底干了什么?

public final override suspend fun join() {
    if (!joinInternal()) { // fast-path no wait
        coroutineContext.ensureActive()
        return // do not suspend
    }
    return joinSuspend() // slow-path wait
}

上面Join函数两个分支,榜首个分支的意思是,依靠的Job现已完毕了,不需求等候了,能够履行回来了。第二个意思是依靠的使命还没有完毕,咱们需求等候。毫无疑问,咱们出问题的代码是走的是第二个分支,那咱们来看看第二个分支到底做了些什么?

private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->
    // We have to invoke join() handler only on cancellation, on completion we will be resumed regularly without handlers
    cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(cont).asHandler))
}

哈哈,这不便是咱们之前讨论的Callback转Suspend函数的代码吗?代码里边cont变量便是代表调用Join函数编译器参加的最终一个参数。咱们能够看到,cont变量给了一个叫ResumeOnCompletion的类,那咱们接着来看ResumeOnCompletion这个类的完结的吧。

private class ResumeOnCompletion(
    private val continuation: Continuation<Unit>
) : JobNode() {
    override fun invoke(cause: Throwable?) = continuation.resume(Unit)
}

咱们找到了那个要害的代码了,continuation.resume(Unit),这个是Join函数回来最要害的代码了,所以我在这儿这个函数上面下了断点,当函数履行到这儿的时分,一切的调用栈清晰可见。原来是被依靠的Job里边有个list,里边放着一切这个Job的Join函数的ResumeOnCompletion,然后在Job完毕的时分,会遍历这个list,然后履行resume函数,然后Join函数就会回来了。这儿的回来仅仅感觉上的回来,假如你看了上面关于suspend的介绍的话,就会知道所谓的回来便是在新状况下从新履行了那个函数了。那这个ResumeOnCompletion是如何放到这个list的呢? 便是通过上面的invokeOnCompletion办法。假如需求更加详尽的了解,能够自己调试一下这个代码。

Kotlin知识点的深入思考

提到这儿,不知道咱们是否意识到之前代码的问题所在了?

问题出现在,由于Join的代码有或许运转在别的的线程,所以当判别所依靠的使命没有完毕,需求等候的时分,把自己的放到list的过程中,还没有放在list里边的那一刹那,Job刚好完毕,然后通知list里边的使命能够从头开端了,可是那个使命刚好没有被放到list里边,所以一旦错过,就成了永远了。

所以吧,Kotlin的官方代码里边,一切的Join函数的履行,都是在launch这个Job的协程中履行的。一个协程,不同的时分,或许会运转在不同的线程上,可是一个协程自身是次序履行的。

好吧,正确的代码如下:

suspend fun build() = coroutineScope {
    val context = coroutineContext
    val job1 = launch(Dispatchers.Default) {
        // start build task 1.
    }
    val job2 = launch(Dispatchers.Default) {
        // start build task 2.
    }
    val job3 = launch(Dispatchers.Default) {
        withContext(context) {
            job1.join()
        }
        // start build task  3.
    }
    val job4 = launch(Dispatchers.Default) {
        withContext(context) {
            job1.join()
            job2.join()
        }
        // start build task 4.
    }
    val job5 = launch(Dispatchers.Default) {
        withContext(context) {
            job2.join()
        }
        // start build task 5.
    }
}

以上的代码通过长期的测试和验证,证明是牢靠的。别的,假如想知道这个问题更详细的布景,请参看 运用程序启动优化新思路 – Kotlin协程

CoroutineContext vs CoroutineScope

这一节聊下在Kotlin协程中一些根本概念,这些知识点自身不难,可是关于初学者来说,比较简单搞混。下面尝试来试着阐明。

首要,先来看一下CoroutineContext,这个比较好了解,便是协程的context。什么叫context,中文一般翻译成上下文,表示一些根本信息。关于Android Application的context,包括包名,版别信息,运用装置路径,运用的工作目录和缓存目录等等根本信息,是描述运用的一些根本信息的。同理协程的context当然便是协程的根本信息。CoroutineContext包括4类信息,如下:

  1. coroutineContext[Job],Job的效果是办理协程的生命周期,和父子协程的联系,能够通过获取。
  2. coroutineContext[ContinuationInterceptor],协程工作线程的办理。
  3. coroutineContext[CoroutineExceptionHandler],错误处理。
  4. coroutineContext[CoroutineName],协程的姓名,一般用作调试。

CoroutineScope这个概念,最开端看的时分和CoroutineContext有点分不清楚。其实你看CoroutineScope的接口代码,里边就包括且仅包括CoroutineContext,本质上,他们其实是一个东西。为什么要规划CoroutineScope呢?尽管这两个本质上是同一个东西,可是他们有不同的规划意图。Context是办理来协程的根本信息,而Scope是用来办理协程的启动。

一般的协程通过launch来启动,launch规划成CoroutineScope的扩展函数,十分有意思的规划是,launch的最终一个参数,新协程的履行体也是一个CoroutineScope的扩展函数。launch函数的榜首个参数是一个Context,launch会把榜首个参数的Context和自身的Context组成一个新的Context,这个新的Context会用来生成新协程的Context,留意这儿不是作为新协程的Context。为什么呢,由于新协程为生成一个Job,这个Job和这个Context组成之后,才是作为新协程的Context。

这儿两个知识点要留意,一切的Context都是Immutable的,假如要修改一个Context,就会新生成一个新的Context。别的,launch的榜首个参数,一般没有指定Job的,一旦指定Job的话,会破会两个协程的父子联系。除非你很确认你要这么做。

一切这些概念的东西,本质上不难,可是关于初学者来说,会感到一头雾水,要深化了解这些概念,需求先去了解一下规划者的规划思路,这样才能够做到事半功倍。

结尾

以上希望能够给咱们一些帮助。别的文章免不了一些忽略和错误,不吝指正。