【译】Kotlin coroutines meeting Android

前言

本文翻译自 Sean McQuillanKotlin coroutines 入门系列。看了他的三篇文章,真正了解了协程出现的意义,它能帮开发者解决的问题,并掌握了它的基本用法。 原文地址:html

协程能够作什么?

对于 Android 开发者来讲,咱们能够将协程运用在如下两个场景:android

  • 耗时任务:咱们不该该在主线程作耗时操做。
  • 主线程安全:咱们能够在主线程中调用 suspend 函数来执行一些操做而不阻塞主线程。

耗时任务 - Callback 实现

咱们都知道不管是请求网络仍是读取数据库都是耗时任务,咱们不能在主线程去执行这些耗时操做。如今的手机 CPU 频率都是很高的,Pixel 2 的单核 CPU 周期小于 0.0000000004 秒(0.4纳秒),而一次网络请求大约是 0.4 秒(400 毫秒)。能够这么说,一眨眼功夫能够完成一次网络请求,但同时 CPU 已经执行了 10 亿屡次。 Android 平台,主线程是 UI 线程,主要负责 View 的绘制(16 ms)和响应用户操做。若是咱们在主线程作耗时操做,就会阻塞主线程,形成 View 不能及时刷新,不能及时响应用户操做,从而影响用户体验。 为了解决以上问题,咱们通常使用 Callbacks 的方式。举个例子:git

class ViewModel: ViewModel() {
   fun fetchDocs() {
       get("developer.android.com") { result ->
           show(result)
       }
    }
}
复制代码

尽管 get() 函数是被主线程调用的,但它的实现确定是要在其余线程完成网络请求的。当结果返回时,Callback 又会在主线程被调用,来将结果显示到 UI 上。github

耗时任务 - 协程实现

协程能够简化异步代码,用协程咱们能够更方便地重写上面的例子:数据库

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.IO
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}
// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}
复制代码

与普通函数相比,协程添加了 suspendresume 两种操做。这两个操做一块儿完成了 Callback 的工做,但更优雅,就像是用同步代码完成了异步操做。编程

  • suspend:挂起当前协程,保存全部的本地变量;
  • resume:恢复已经挂起的协程,从它暂停的地方继续执行。

suspend 是 Kotlin 的一个关键字。被 suspend 标记的函数,只能在 suspend 函数内被调用。咱们可使用协程提供的 launchasync 从主线程启动一个协程来执行 suspend 函数。安全

public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

public fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
复制代码

Coroutines 工做过程

如动画所示,get() 函数在执行前会挂起(suspend)当前的协程,它内部依旧是经过 IO 线程(Dispatchers.IO)来执行网络请求。当请求完成时,它不是经过 Callback 的方式,而是恢复(resume)已经挂起的协程继续执行 show(result) 函数。任何一个协程被挂起时,当前的栈信息都会被复制并保存,以便在恢复时使用。当全部的协程都被挂起时,主线程不会被阻塞,仍然能够更新 UI 和响应用户操做。因而可知,协程为咱们提供了一种异步操做的简单实现方式。网络

主线程安全

使用 Kotlin 协程的一个原则是:咱们应该保证咱们写的 suspend 函数是主线程安全的,也就是能够在任何线程中调用它,而不用去让调用者手动切换线程。 须要注意的是:suspend 函数通常是运行在主线程中的,suspend 不是意味着运行的子线程。也就是说,咱们须要在 suspend 内部指定该函数执行的线程,如不指定,它默认运行在调用者的线程。 若是不是执行耗时任务,咱们可使用 Dispatchers.Main.immediate 来启动一个协程,下一次 UI 刷新时就会将结果显示到 UI 上。 全部的 Kotlin协程都必须运行在一个 Dispatcher 中,它提供了如下几种 Dispatcher 来运行协程。并发

Dispatchers 用途 使用场景
Dispatchers.Main 主线程、UI交互、执行轻量任务 Call suspend functions, Call UI functions, Update LiveData
Dispatchers.IO 网络请求、文件访问 Database, Reading/writing files, Networking
Dispatchers.Default CPU密集型任务 Sorting a list, Parsing JSON, DiffUtils
Dispatchers.Unconfined 不限制任何指定线程 限制恢复后的线程

完整的 get() 函数以下所示:app

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}
// Dispatchers.Main
suspend fun get(url: String) =
    // Dispatchers.IO
    withContext(Dispatchers.IO) {
        // Dispatchers.IO
        /* perform blocking network IO here */
    }
    // Dispatchers.Main
复制代码

使用协程咱们能够自由控制代码运行的线程,withContext() 为咱们提供了相似编写同步代码的方式来实现异步编程。如上面提到的:咱们应尽可能使用 withContext 确保每一个函数都是线程安全的,不要让调用者关心要在哪一个线程才能调用该函数。 上面的例子中,fetchDocs 运行在主线程,而 get() 运行在子线程,因为协程的 挂起/恢复 机制,当 withContext 返回时,当前协程会恢复执行。 在性能方面,withContext 跟 Callbacks 或 RxJava 不相上下。因此咱们不用担忧性能问题,相信官方也会持续优化。

结构化并发(Structured concurrency)

协程相比于线程来讲,它是很轻量的。咱们能够启动上百上千个协程,但没法启动这么多的线程。虽然协程很轻量,但它们的实际进行的任务多是耗时的,好比用于读取数据库、请求网络或读写文件的协程。所以,咱们仍须要维护好这些协程的完成和取消,不然可能发生任务泄露,这比内存泄漏更严重,由于任务可能浪费 CPU、磁盘或网络资源。 手动管理成百上千个协程是很困难的,为了不协程泄露,Kotlin 提供了 结构化并发 来帮助咱们更方便地追踪全部运行中的协程。在 Android 开发过程当中,咱们能够用它来完成如下三件事:

  • 取消 再也不须要的任务
  • 追踪 全部运行中的任务
  • 接收协程的 异常

取消限定范围内的任务(Cancel work with scopes)

Kotlin 协程必须运行在 CoroutineScope 中,CoroutineScope 能够追踪全部运行中和已挂起的协程,不像上文提到的 Dispatchers,它只是保证对全部的协程的追踪,而不会真正地执行它们。所以为了确保全部的协程都能被追踪到,咱们不能在 CoroutineScope 外启动一个新的协程。同时咱们可使用 CoroutineScope 来取消在它内部启动的全部协程。 咱们须要在普通函数中启动一个协程,才能调用 suspend 函数。协程提供了两种方式来启动一个新的协程。

  • launch:启动一个新协程,可是没法得到它执行的结果。
  • async:启动一个新协程,能够经过调用它的 await() 函数得到协程的执行结果。

大多数状况下,咱们使用 launch 来启动一个新的协程。launch 函数就像链接普通函数和协程的桥梁。

scope.launch {
    // This block starts a new coroutine
    // "in" the scope.
    //
    // It can call suspend functions
   fetchDocs()
}
复制代码

launchasync 最大的不一样就是它们处理异常的方式:launch 启动的协程在发生异常时会马上抛出,并马上取消全部协程;而 async 启动的协程,只有咱们调用 await() 函数时才能获得内部的异常,若无异常会返回执行结果。

AndroidX Lifecycle KTX 为咱们提供了 viewModelScope 来方便地在 ViewModel 中启动协程,并保持对它们的追踪。

class MyViewModel(): ViewModel() {
    fun userNeedsDocs() {
        // Start a new coroutine in a ViewModel
        viewModelScope.launch {
            fetchDocs()
        }
    }
}
复制代码

更多详情可查看Kotlin coroutines meeting Architecture components

咱们能够在一个 CoroutineScope 中包含若干个 CoroutineScope,若是咱们在一个协程中启动了另外一个协程,其实它们最终都同属于一个最顶层的 CoroutineScope,也就是说咱们能够经过取消最外层的协程来取消全部内部的协程。 若是咱们取消一个已经挂起的协程,它会抛出一个异常 CancellationException。若是咱们捕获并消费了这个异常,或者取消一个未挂起的协程,该协程会处于一个 半取消(semi-canceled)状态。 viewModelScope 启动的协程会在 ViewModel 销毁(clear)时自动取消,因此即便咱们其内部执行是一个死循环,也会被自动取消。

fun runForever() {
    // start a new coroutine in the ViewModel
    viewModelScope.launch {
        // cancelled when the ViewModel is cleared
        while(true) {
            delay(1_000)
            // do something every second
        }
    }
}
复制代码

追踪进行中的任务(Keep track of work)

咱们可使用协程进行网络请求、读写数据库等耗时操做。但有时咱们可能须要在一个协程中同时进行两个网络请求,这时咱们须要再启动两个协程来共同工做。咱们能够在任何一个 suspend 函数中使用 coroutineScopesupervisorScope 来启动更多的协程。 在一个协程中启动新的协程可能会形成潜在的任务泄露,由于调用者可能不知道咱们内部的实现。好消息是,结构化并发能够保证:若是一个 suspend 函数返回了,那么它内部的全部代码都已经执行完毕。 这仍然是同步调用的影子。 举个例子:

suspend fun fetchTwoDocs() {
    coroutineScope {
        launch { fetchDoc(1) }
        async { fetchDoc(2) }
    }
}
复制代码

如上所示:fetchTwoDocs() 内部经过 coroutineScope 又启动了两个协程来同时加载两个文档,一个方式是 launch,一种方式是 async。为了不 fetchTwoDocs() 任务泄露,coroutineScope 会一直保持挂起状态,直到内部的全部协程都执行完毕,这时 fetchTwoDocs() 函数才会返回。

coroutineScope keep track of 1_000 coroutines

以上示例,咱们同时启动了 1000 个协程来请求网络,loadLots() 内部的 coroutineScope 是 该函数调用者的 CoroutineScope 的子集,内部的 coroutineScope 会一直保持这 1000 个协程的追踪,只有当全部协程都执行完毕,loadLots() 函数才会返回。

coroutineScopesupervisorScope 可让咱们在任意 suspend 函数内安全启动协程,直到内部的全部协程都执行完毕,它们才会返回。此外,若是咱们取消了外层的 scope,内部的子协程也会被取消。 coroutineScopesupervisorScope 的区别是:只要 coroutineScope 内的任一协程执行失败,整个 scope 都会被取消,内部的其余子协程也会马上被取消;而 supervisorScope 内的某一协程失败,不会取消其余的子协程。

接收协程执行失败抛出的异常(Signal errors when a coroutine fails)

和普通函数同样,协程在执行失败时也会抛出异常。suspend 函数内抛出的异常是会向上传递的,咱们也可使用 try/catch 语法或其余方式捕获异常。可是下面这种异常可能会丢失:

val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
    // async without structured concurrency
    unrelatedScope.async {
        throw InAsyncNoOneCanHearYou("except")
    }
}
复制代码

以上代码中,咱们在一个不相关的限定范围内启动了一个协程,它并非结构化并发的。因为 async 函数启动的协程只有在调用 await() 时才会抛出异常,因此这个异常可能会丢失,它会被一直保存着直到咱们调用 await()结构化并发能够保证当一个协程发生异常时,它的调用者或 scope 能够收到这个异常。 上面代码用结构化并发的方式改写以下:

suspend fun foundError() {
    coroutineScope {
        async {
            throw StructuredConcurrencyWill("throw")
        }
    }
}
复制代码

总结一下:

  • coroutineScopesupervisorScope 是结构化并发的,能够追踪内部的全部协程,包括异常处理、任务取消等。
  • GlobalScope 不是结构化并发的,它是一个全局的 scope,跟 Application 同生命周期。

Kotlin Coroutines VS RxJava&RxAndroid

Kotlin Coroutines 与 RxJava&RxAndroid 均可以方便的帮咱们进行异步编程,我的以为它们在异步编程最大的区别是:Coroutines 的编写方式更像是同步调用,而 RxJava 是流式编程。但本质上,它们内部都是经过线程池来处理耗时任务。RxJava 的有不少个操做符能够辅助实现各式各样的需求,并能保证链式调用;Coroutines 是与 Kotlin 结合的最好异步编程方式,目前也有不少的官方支持,相信未来 Coroutines 会有很好的使用体验和执行性能。

Reference

联系

我是 xiaobailong24,您能够经过如下平台找到我:

相关文章
相关标签/搜索