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

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

追踪协程


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

  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 中返回是不可能的。


许多许多任务


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

看一下下面的动画:

image.png

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


上面的代码中,我们在 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 中启动协程,以及如何使用结构化并发来优化代码。

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



相关文章
|
2月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
38 1
|
3月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
106 1
|
4月前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
66 4
|
5月前
|
开发者 Kotlin Android开发
Kotlin协程在Android开发中的应用
【7月更文挑战第10天】Kotlin协程简化了Android异步编程,提供轻量级并发。挂起函数让异步代码看起来同步,不阻塞线程,便于管理。在项目中,添加Kotlin和协程依赖,如`kotlinx.coroutines-core`和`kotlinx-coroutines-android`。使用`CoroutineScope`和`launch`处理耗时任务,如网络请求,避免主线程阻塞。挂起函数和调度器控制执行上下文,适应不同任务需求。
|
6月前
|
安全 Android开发 Kotlin
Android面试题之Kotlin协程并发问题和互斥锁
Kotlin的协程提供轻量级并发解决方案,如`kotlinx.coroutines`库。`Mutex`用于同步,确保单个协程访问共享资源。示例展示了`withLock()`、`lock()`、`unlock()`和`tryLock()`的用法,这些方法帮助在协程中实现线程安全,防止数据竞争。
87 1
|
6月前
|
存储 Java 调度
Android面试题之Kotlin 协程的挂起、执行和恢复过程
了解Kotlin协程的挂起、执行和恢复机制。挂起时,状态和上下文(局部变量、调用栈、调度器等)被保存;挂起点通过`Continuation`对象处理,释放线程控制权。当恢复条件满足,调度器重新分配线程,调用`resumeWith`恢复执行。关注公众号“AntDream”获取更多并发知识。
140 2
|
7月前
|
JSON Android开发 开发者
构建高效Android应用:采用Kotlin协程优化网络请求
【5月更文挑战第31天】 在移动开发领域,尤其是针对Android平台,网络请求的管理和性能优化一直是开发者关注的焦点。随着Kotlin语言的普及,其提供的协程特性为异步编程提供了全新的解决方案。本文将深入探讨如何利用Kotlin协程来优化Android应用中的网络请求,从而提升应用的响应速度和用户体验。我们将通过具体实例分析协程与传统异步处理方式的差异,并展示如何在现有项目中集成协程进行网络请求优化。
|
6月前
|
JSON 安全 调度
Android面试题之Kotlin协程一文搞定
本文介绍了协程的基础知识,强调它是轻量级线程,用于处理耗时任务而不阻塞主线程,确保主线程安全。协程特点包括使异步逻辑同步化,并允许函数挂起和恢复。挂起函数由`suspend`关键字标识,只能在协程内部调用。挂起与阻塞的主要区别在于挂起不会导致主线程ANR。 结构化并发和协程作用域(如`CoroutineScope`、`GlobalScope`、`MainScope`等)提供了任务管理,文章还探讨了并发、启动模式、协程取消、超时任务以及资源释放等主题。
81 0
|
6月前
|
存储 Java 调度
Android面试题之Kotlin协程到底是什么?它是线程吗?
本文探讨了协程与线程的区别,指出协程并非线程,而是轻量级的线程替代。协程轻量体现在它们共享调用栈,内存占用少,仅需几个KB。协程切换发生在用户态,避免了昂贵的内核态切换。在Kotlin中,协程通过Continuation对象实现上下文保存,允许高效并发执行,而不会像线程那样消耗大量资源。通过`runBlocking`和`launch`示例展示了协程的非阻塞挂起特性。总结来说,协程的轻量主要源于内存占用少、切换开销低和高并发能力。
126 0
|
7月前
|
Java Android开发 开发者
构建高效Android应用:Kotlin协程的实践指南
【5月更文挑战第31天】在现代Android开发中,异步编程和性能优化成为关键要素。Kotlin协程作为一种在JVM上实现轻量级线程的方式,为开发者提供了简洁而强大的并发处理工具。本文深入探讨了如何在Android项目中利用Kotlin协程提升应用的响应性和效率,包括协程的基本概念、结构以及实际运用场景,旨在帮助开发者通过具体实例理解并掌握协程技术,从而构建更加流畅和高效的Android应用。