[译] 在 Android 使用协程(part I) - 协程的背景知识

这是「怎样在 Android 上使用协程」的系列文章的第一篇。

这篇内容关注协程怎么工作的以及它们解决什么问题。

协程解决什么问题

Kotlin 的协程采用了一种新的并发方式(a new style of concurrency),可以在 Android 上简化异步代码。

虽然在 Kotlin 1.3 协程作为全新特性出现的,但是协程的概念从编程语言诞生之初就已经存在了。第一个探索使用协程的语言是的 Simula ,出现在 1967年。

最近几年,协程越来越受欢迎,现在许多流行的编程语言里都有协程,如 Javascript、C#、Python、Ruby等等。Kotlin 协程设计基于这些构建过大型应用的经验。

在 Android 上,协程可以非常好的解决两个问题:

  1. 防止耗时任务在主线程运行过久,阻塞主线程
  2. 从主线程上安全地去调用网络或磁盘操作

让我们深入这两个问题,看看协程是如何帮助我们写出更简洁的代码。

耗时任务

访问网页或和 API 交互都需要访问网络。同样的,访问数据库或从硬盘加载图片都需要读取文件。这些就是我说的耗时任务——这些任务耗时太长,导致你的应用卡顿。

很难想象,现代手机执行代码相比网络请求有多快。在 Pixel 2上,一次 CPU 周期只需要 0.0000004 秒,这个数字从人类的角度上很难理解。然而,如果你把一次网络请求看成一眨眼的时间,差不多 400 毫秒(0.4秒),这就比较好理解 CPU 执行有多快了。一次眨眼的时间,或者稍微慢一点的网络请求中, CPU 可以执行超过 100万个周期。

在 Android 上,每个应用程序都有一个主线程负责处理 UI (比如绘制视图)和与用户交互。如果在这个线程上做了太多工作,应用程序就会出现卡顿或者响应缓慢,从而导致不好的用户体验。任何耗时任务都不应该阻塞主线程,这样你的应用就能避免例如触摸反馈时响应缓慢的卡顿。

为了在主线程以外执行网络请求,一个常见的模式是 Callback,Callback 给了 library 一个 handle,它可以用来在将来的某个时候调用你的代码。使用 Callback 访问 developer.android.com 看起来类似这样:

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

即使在主线程调用 get ,它也会在另一个线程执行网络请求。然后,一旦从网络中获取到结果,就会在主线程上调用回调。这是处理耗时任务的好办法,而通过 Retrofit 可以帮助你在其他线程发出网络请求。

使用协程做耗时操作

协程可以简化耗时任务,例如 fetchDocs 的代码。为了展示协程如何简化耗时任务的代码,让我们使用协程来重写上面的 Callback 示例。

// 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){/*...*/}
复制代码

为什么这个代码不会阻塞主线程?它怎么在不等待网络请求和阻塞的情况下返回 get 的结果?事实证明,协程为 Kotlin 提供了一种方式来执行这段代码,并且不会阻塞主线程。

协程在常规函数的基础上加了两个新的操作符。除了 invoke (or call) 和 return 以外,协程还添加了 suspend 和 resume。

  • suspend——暂停当前协程的执行,保存所有本地变量
  • resume——让挂起的协程从暂停的地方恢复执行

Kotlin 通过函数上的 suspend 关键字来添加这个功能。你只能从其他挂起函数调用挂起函数,或者使用协程启动器类似 launch 来启动一个新的协程。

挂起配合上恢复代替回调

Suspend and resume work together to replace callbacks.

在上面的例子中,get 会在启动网络请求之前被 挂起 。然后get函数会脱离主线程继续负责运行网络请求。然后,当网络请求完成时,它不用回调来通知主线程,而是简单的 恢复 挂起的协程。

![显示 Kotlin 如何实现 挂起 和 恢复来替换回调的动画。](Coroutines on Android (part I) Getting the background.assets/16abe1535c453a63.gif)

查看fetchDocs是如何执行的,你可以看到 挂起 是如何工作的。当协程被挂起时,当前堆栈帧(Kotlin 用来跟踪某个函数正在运行的位置及其变量)将会被复制并保存,用来以后使用。当它恢复,这个堆栈帧会被复制回来,并重新运行。在动画的中间——当主线程上所有协程被挂起时,主线程可以自由地更新屏幕并处理用户事件。挂起配合恢复替换了回调,非常简洁。

当主线程上的所有协程都挂起时,主线程可以自由地执行其他工作。

When all of the coroutines on the main thread are suspended, the main thread is free to do other work.

即使我们编写了与阻塞网络请求完全相同的直接顺序的代码,协程也将按照我们希望的方式运行我们的代码,并且避免阻塞了主线程!

接下来,让我们看看如何使用协程实现主线程安全(main-safety),并探索调度流程。

主线程安全与协程

在 Kotlin 协程中,编写合适的挂起函数从主线程调用总是安全的。无论它们会做什么,都应该始终允许任何线程可以去调用它们。

但是,我们在安卓应用中做的很多事情,对于主线程来说都太慢了。网络请求、解析 JSON 、读写数据库,甚至只是遍历大型列表。其中任何一个都有可能因为太慢导致用户可以察觉到的延迟,所以应该脱离主线程运行。

使用 挂起 不是告诉 Kotlin 在一个后台线程挂起。值的一提的说,协程将在主线程运行。(注:原文 It’s worth saying clearly and often that coroutines will run on the main thread. )实际上,在响应一个 UI 事件的时候,使用 Dispatchers.Main.immediate 启动一个协程是一个非常好的主意——这样,如果你最终没有在主线程执行耗时任务,那么结果就会在下一帧提供给用户。

协程讲运行在主线程,并且挂起不代表在后台

Coroutines will run on the main thread, and suspend does not mean background.

这样的方式操作一个函数,会让主线程变慢,你可以告诉 Kotlin 协程在 Default 调度器或者 IO 调度器上执行工作。

在 Kotlin 中,所有的协程必须通过调度器运行,即使它们运行在主线程上。协程可以 挂起 自己,而调度器知道怎么 恢复 它们。

要指定协程应该运行在哪里,Kotlin 提供了三个可以用于切换线程的调度器。

+-----------------------------------+
|         Dispatchers.Main          |
+-----------------------------------+
| Main thread on Android, interact  |
| with the UI and perform light     |
| work                              |
+-----------------------------------+
| - Calling suspend functions       |
| - Call UI functions               |
| - Updating LiveData               |
+-----------------------------------+

+-----------------------------------+
|          Dispatchers.IO           |
+-----------------------------------+
| Optimized for disk and network IO |
| off the main thread               |
+-----------------------------------+
| - Database*                       |
| - Reading/writing files           |
| - Networking**                    |
+-----------------------------------+

+-----------------------------------+
|        Dispatchers.Default        |
+-----------------------------------+
| Optimized for CPU intensive work  |
| off the main thread               |
+-----------------------------------+
| - Sorting a list                  |
| - Parsing JSON                    |
| - DiffUtils                       |
+-----------------------------------+
复制代码

* 如果你使用 挂起函数, RxJava, 或 LiveData,Room 会提供自动的主线程安全。

** 网络库(如 Retrofit 和 Volley)会管理它们自己的线程,当与 Kotlin 协程一起使用时,不需要在代码中显式地声明主线程安全。

继续上面的示例,让我们使用调度器来定义 get 函数。在 get 的函数体中,我们调用 withContext(Dispatchers.IO) 用来创建一个运行在 IO 调度器 的代码块。你写在这个代码块中的所有代码都始终将在 IO 调度器上运行。由于 withContext 本身是一个挂起函数,所以它将使用协程来保证主线程安全。

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

使用协程,你可以对线程进行细粒度的划分(With coroutines you can do thread dispatch with fine-grained control)。因为withContext 允许你控制在什么线程上执行任何代码块,而不需要引入回来返回结果,所以你可以将它应用于非常小的函数,比如从数据库读取数据或执行网络请求。因此,一个好的实践是使用 withContext 来确保任何调度器(包括 Main)上调用每个函数都是安全的——这样调用者就不必考虑需要在哪个线程执行函数。

在这个例子上,fetchDocs 在主线程上执行,但是可以安全的调用get 函数,然后会在后台执行网络请求。因为协程支持挂起和恢复,所以只要 withContext块完成,主线程上的协程就会被恢复得到结果。

好的挂起函数从主线程调用总是安全的。

* Well written suspend functions are always safe to call from the main thread (or main-safe).

让每个挂起函数在主线程调用都是安全的是个好主意。如果它做了任何触及磁盘、网络甚至只是占有太多 CPU 的操作,那么就使用 withContext 来确保从主线程调用是安全。这是基于协程的库(如 Retrofit 和 Room)所遵循的模式。如果你在整个代码库中都遵循这种风格,那么你的代码将会简单的多,并避免将线程问题和应用程序逻辑混合在一起。当遵循这个模式时,协程可以在主线程上自由调用,用简单的代码请求网络或数据库,同时保证用户不会看到卡顿。

withContext 的性能

对于提供主线程安全上,withContext 使用回调或者 RxJava 一样快。在某些情况下,withContext 可能通过优化甚至比回调性能还好。如果一个函数将要访问10次数据库,你可以告诉 Kotlin 使用 withContext 在 10 次的调用的外部切换一次(原文:If a function will make 10 calls to a database, you can tell Kotlin to switch once in an outer withContext around all 10 calls. )。然后,即使数据库会重复调用 withContext ,它也会保持在同一个调度器上,并遵循快速路径。此外在 Default 调度器和 IO 调度器直接切换经过优化会尽可能的避免线程切换。

下篇是什么

在这篇文章中,我们探讨了协程最擅长解决的问题。协程在编程语言中是一个存在很久的概念,由于它能够简化与网络交互的代码,所以最近变得非常流行。

在 Android 上,你可以使用它们解决两个非常常见的问题:

  1. 简化耗时任务的代码,比如从网络、磁盘读取数据,甚至解析大型 JSON 结果。
  2. 执行精确的主线程安全,以确保不会意外阻塞主线程,而不会使代码难以读和写(原文:Performing precise main-safety to ensure that you never accidentally block the main thread without making code difficult to read and write)。

在下一篇文章,我们探索它们是如何配合 Android 的,以便你使用它(原文:In the next post we’ll explore how they fit in on Android to keep track of all the work you started from a screen! Give it a read:)。

Coroutines on Android (part II): Getting started

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值