在 Android 上使用协程(二):Getting started

原文做者 :Sean McQuillanhtml

原文地址: Coroutines on Android (part II): Getting startedandroid

译者 : 秉心说git

这是关于在 Android 中使用协程的一系列文章。本篇的重点是开始任务以及追踪已经开始的任务。github

上一篇 :数据库

在 Android 上使用协程(一):Getting The Background编程

协程解决了什么问题?安全

追踪协程

在上篇文章中,咱们探索了协程擅长解决的问题。一般,协程对于下面两个常见的编程问题来讲都是不错的解决方案:微信

  1. 耗时任务,运行时间过长阻塞主线程
  2. 主线程安全,容许你在主线程中调用任意 suspend(挂起) 函数

为了解决这些问题,协程基于基础函数添加了 suspendresume。当特定线程上的全部协程都被挂起,该线程就能够作其余工做了。网络

可是,协程自己并不能帮助你追踪正在进行的任务。同时拥有并挂起数百甚至上千的协程是不可能的。尽管协程是轻量的,但它们执行的任务并非,例如文件读写,网络请求等。并发

使用代码手动追踪一千个协程的确是很困难的。你能够尝试去追踪它们,而且手动保证它们最后会完成或者取消,可是这样的代码冗余,并且容易出错。若是你的代码不够完美,你将失去对一个协程的追踪,我把它称之为任务泄露。

任务泄露就像内存泄露同样,并且更加糟糕。对于已经丢失泄露的协程,除了内存消耗以外,它还会恢复本身来消耗 CPU,磁盘,甚至启动一个网络请求。

泄露的协程会浪费内存,CPU,磁盘,甚至发送一个不须要的网络请求。

为了不泄露协程,Kotlin 引入了 structured concurrency(结构化并发)。结构化并集合了语言特性和最佳实践,遵循这个原则将帮助你追踪协程中的全部任务。

在 Android 中,咱们使用结构化并发能够作三件事:

  1. 取消再也不须要的任务
  2. 追踪全部正在进行的任务
  3. 协程失败时的错误信号

让咱们深刻探讨这几点,来看看结构化并发是如何帮助咱们避免丢失对协程的追踪以及任务泄露。

经过做用域取消任务

在 Kotlin 中,协程必须运行在 CoroutineScope 中。CoroutineScope 会追踪你的协程,即便协程已经被挂起。不一样于上一篇文章中说过的 Dispatchers,它实际上并不执行协程,它仅仅只是保证你不会丢失对协程的追踪。

为了保证全部的协程都被追踪到,Kotlin 不容许你在没有 CoroutineScope 的状况下开启新的协程。你能够把 CoroutineScope 想象成具备特殊能力的轻量级的 ExecutorServicce。它赋予你建立新协程的能力,这些协程都具有咱们在上篇文章中讨论过的挂起和恢复的能力。

CoroutineScope 会追踪全部的协程,而且它也能够取消全部由他开启的协程。这很适合 Android 开发者,当用户离开当前页面后,能够保证清理掉全部已经开启的东西。

CoroutineScope 会追踪全部的协程,而且它也能够取消全部由他开启的协程。

启动新的协程

有一点须要注意的是,你不是在任何地方均可以调用挂起函数。挂起和恢复机制要求你从普通函数切换到协程。

启动协程有两种方法,且有不一样的用法:

  1. 使用 launch 协程构建器启动一个新的协程,这个协程是没返回值的
  2. 使用 async 协程构建器启动一个新的协程,它容许你返回一个结果,经过挂起函数 await 来获取。

在大多数状况下,如何从一个普通函数启动协程的答案都是使用 launch。由于普通函数是不能调用 await 的(记住,普通函数不能直接调用挂起函数)。稍后咱们会讨论何时应该使用 async

你应该调用 launch 来使用协程做用域启动一个新的协程。

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

你能够把 launch 想象成一座桥梁,链接了普通函数中的代码和协程的世界。在 launch 内部,你能够调用挂起函数,而且建立主线程安全性,就像上篇文章中提到的那样。

Launch 是把普通函数带进协程世界的桥梁。

提示:launchasync 很大的一个区别是异常处理。async 指望你经过调用 await 来获取结果(或异常),因此它默认不会抛出异常。这就意味着使用 async 启动新的协程,它会悄悄的把异常丢弃。

因为 launchasync 只能在 CoroutineScope 中使用,因此你建立的每个协程都会被协程做用域追踪。Kotlin 不容许你建立未被追踪的协程,这样能够有效避免任务泄露。

在 ViewModel 中启动

若是一个 CoroutineScope 追踪在其中启动的全部协程,launch 会新建一个协程,那么你应该在何处调用 launch 并将其置于协程做用域中呢?还有,你应该在何时取消在做用域中启动的全部协程呢?

在 Android 中,一般将 CoroutineScope 和用户界面相关联起来。这将帮助你避免协程泄露,而且使得用户再也不须要的 Activity 或者 Fragment 再也不作额外的工做。当用户离开当前页面,与页面相关联的 CoroutineScope 将取消全部工做。

结构化并发保证当协程做用域取消,其中的全部协程都会取消。

当经过 Android Architecture Components 集成协程时,通常都是在 ViewModel 中启动协程。这里是许多重要任务开始工做的地方,而且你没必要担忧旋转屏幕会杀死协程。

为了在 ViewModel 中使用协程,你能够来自 lifecycle-viewmodel-ktx:2.1.0- alpha04 这个库的 viewModelScopeviewModelScope 即将在 Android Lifecycle v2.1.0 发布,如今仍然是 alpha 版本。关于 viewModelScope 的原理能够阅读 这篇博客。既然这个库目前仍是 alpha 版本,就可能会有 bug,API 也可能发生变更。若是你找到了 bug,能够在 这里 提交。

看一下使用的例子:

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

viewModelScope 被清除(即 onCleared() 被调用)时,它会自动取消由它启动的全部协程。这确定是正确的行为,当咱们尚未读取到文档,用户已经关闭了 app,咱们还继续请求的话只是在浪费电量。

为了更高的安全性,协程做用域会自动传播。若是你启动的协程中又启动了另外一个协程,它们最终会在同一个做用域中结束。这就意味着你依赖的库经过你的 viewModelScope 启动了新的协程,你就有办法取消它们了!

Warning: Coroutines are cancelled cooperatively by throwing a CancellationException when the coroutine is suspended. Exception handlers that catch a top-level exception like Throwable will catch this exception. If you consume the exception in an exception handler, or never suspend, the coroutine will linger in a semi-canceled state.(这段没有理解)

因此,当你须要协程和 ViewModel 的生命周期保持一致时,使用 viewModelScope 来从普通函数切换到协程。那么,因为 viewModelScope 会自动取消协程,编写下面这样的无限循环是没有问题的,不会形成泄露。

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
        }
    }
}
复制代码

使用 viewModelScope,你能够确保任何工做,即便是死循环,都能在再也不须要执行的时候将其取消。

追踪任务

启动一个协程是没问题的,不少时候也正是这样作的。经过一个协程,进行网络请求,保存数据到数据库。

有时候,状况会稍微有点复杂。若是你想在一个协程中同时进行两个网络请求,你就须要启动更多的协程。

为了启动更多的协程,任何挂起函数均可以使用 coroutineScope 或者 supervisorScope 构建器来新建协程。这个 API,说实话有点让人困惑。coroutineScope 构建器和 CoroutineScope 是两个不一样的东西,却只有一个字母不同。

在任何地方启动新协程,这可能会致使潜在的任务泄露。调用者可能都不知道新协程的启动,它又如何其跟踪呢?

结构化并发帮助咱们解决了这个问题。它给咱们提供了一个保障,保证当挂起函数返回时,它的全部工做都已经完成。

结构化并发保证当挂起函数返回时,它的全部任务都已经完成。

下面是使用 coroutineScope 来查询文档的例子:

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

在这个例子中,同时从网络读取两个文档。第一个是在由 launch 启动的协程中执行,它不会给调用者返回任何结果。

第二个使用的是 async,因此文档能够返回给调用者。这里例子有点奇怪,一般两个文档都会使用 async。可是我只是想向你展现你能够根据你的需求混合使用 launchasync

coroutineScope 和 supervisorScope 让你能够安全的在挂起函数中启动协程。

尽管上面的代码没有在任何地方显示的声明要等待协程的执行完成,看起来当协程还在运行的时候,fetDocs 方法就会返回。

为告终构化并发和避免任务泄露,咱们但愿确保当挂起函数(例如 fetchDocs)返回时,它的全部任务都已经完成。这就意味着,由 fetchDocs 启动的全部协程都会先于它返回以前执行结束。

Kotlin 经过 coroutineScope 构建器确保 fetchDocs 中的任务不会泄露。coroutineScope 构建器直到在其中启动的全部协程都执行结束时才会挂起本身。正因如此,在 coroutineScope 中的全部协程还没有结束以前就从 fetchDocs 中返回是不可能的。

许多许多任务

如今咱们已经探索了如何追踪一个和两个协程,如今是时候来尝试追踪一千个协程了!

看一下下面的动画:

Animation showing how a coroutineScope can keep track of one thousand coroutines.

这个例子展现了同时进行一千次网络请求。这在真实的代码中是不建议的,会浪费大量资源。

上面的代码中,咱们在 coroutineScope 中经过 launch 启动了一千个协程。你能够看到它们是如何链接起来的。因为咱们是在挂起函数中,因此某个地方的代码必定是使用了 CoroutineScope 来启动协程。对于这个 CoroutineScope,咱们一无所知,它多是 viewModelScope 或者定义在其余地方的 CoroutineScope。不管它是什么做用域,coroutineScope 构建器都会把它当作新建做用域的父亲。

coroutineScope 代码块中,launch 将在新的做用域中启动协程。当协程完成启动,这个新的做用域将追踪它。最后,一旦在 coroutineScope 中启动的全部协程都完成了,loadLots 就能够返回了。

Note: the parent-child relationship between scopes and coroutines is created using Job objects. But you can often think of the relationship between coroutines and scopes without diving into that level.

coroutineScope 和 supervisorScope 会等待全部子协程执行结束。

这里有不少事情在进行,其中最重要的就是使用 coroutineScope 或者 supervisorScope,你能够在任意挂起函数中安全的启动协程。尽管这将启动一个新协程,你也不会意外的泄露任务,由于只有全部新协程都完成了你才能够挂起调用者。

很酷的是 coroutineScope 能够建立子做用域。若是父做用域被取消,它会将取消动做传递给全部的新协程。若是调用者是 viewModelScope,当用户离开页面是,全部的一千个协程都会自动取消。多么的整洁!

在咱们移步谈论异常处理以前,有必要来讨论一下 coroutineScopesupervisorScope。它们之间最大的不一样就是,当其中任意一个子协程失败时,coroutineScope 会取消。因此,若是一个网络请求失败了,其余的全部请求都会马上被取消。若是你想继续执行其余请求的话,你可使用 supervisorScope,当一个子协程失败时,它不会取消其余的子协程。

协程失败的异常处理

在协程中,错误也是用过抛出异常来发出信号,和普通函数同样。挂起函数的异常将在 resume 的时候从新抛出给调用者。和普通函数同样,你不会被限制使用 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,这个错误将永远被保存,静静的等待被发现。

结构化并发保证当一个协程发生错误,它的调用者或者做用域能够发现。

若是咱们使用结构化并发写上面的代码,异常将会正确的抛给调用者。

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

因为 coroutineScope 会等待全部子协程执行完成,因此当子协程失败时它也会知道。当 coroutineScope 启动的协程抛出了异常,coroutineScope 会将异常扔给调用者。若是使用 coroutineScope 代替 supervisorScope,当异常抛出时,会马上中止全部的子协程。

使用结构化并发

在这篇文章中,我介绍告终构化并发,以及在代码中配合 ViewModel 使用来避免任务泄露。我还谈论了它是如何让挂起函数更加简单。二者都确保在返回以前完成任务,也能够确保正确的异常处理。

咱们使用非结构化并发,很容易形成意外的任务泄露,这对调用者来讲是未知的。任务将变得不可取消,也不能保证异常被正确的抛出。这会致使咱们的代码产生一些模糊的错误。

使用未关联的 CoroutineScope(注意是大写字母 C),或者使用全局做用域 GlobalScope ,会致使非结构化并发。只有在少数状况下,你须要协程的生命周期长于调用者的做用域时,才考虑使用非结构化并发。一般状况下,你都应该使用结构化并发来追踪协程,处理异常,拥有良好的取消机制。

若是你有非结构化并发的经验,那么结构化并发的确须要一些时间来适应。这种保障使得和挂起函数交互更加安全和简单。咱们应该尽量的使用结构化并发,由于它使得代码更加简单和易读。

在文章的开头,我列举告终构化并发帮助咱们解决的三个问题:

  1. 取消再也不须要的任务
  2. 追踪全部正在进行的任务
  3. 协程失败时的错误信号

结构化并发给予咱们以下保证:

  1. 看成用域取消,其中的协程也会取消
  2. 当挂起函数返回,其中的全部任务都已完成
  3. 当协程发生错误,其调用者会获得通知

这些加在一块儿,使得咱们的代码更加安全,简洁,而且帮助咱们避免任务泄露。

What's Next?

这篇文章中,咱们探索了如何在 Android 的 ViewModel 中启动协程,以及如何使用结构化并发来优化代码。

下一篇中,咱们将更多的讨论在特定状况下使用协程。

文章首发微信公众号: 秉心说 , 专一 Java 、 Android 原创知识分享,LeetCode 题解。

更多 JDK 源码解析,扫码关注我吧!

相关文章
相关标签/搜索