40

(译)协程在 Android 上的应用 (第一部分): 了解背景

 4 years ago
source link: http://jinyulei.cn/kotlin/async/2019/05/14/Coroutines-on-Android-(part-I)-Getting-the-background/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

原文链接

这是关于在Android上使用协程的一系列文章的一部分。这篇文章重点介绍了协程如何工作以及它们解决了哪些问题。

协程解决了哪些问题

Kotlin 协程引入了一种新的并发风格,可以在 Android 上用于简化异步代码。虽然它们是Kotlin 1.3 版中的新特性,但自编程语言诞生以来,协程的概念就已存在。Simula于1967年首次使用协程。

在过去几年中,协程越来越受欢迎,现在已经包含在许多流行的编程语言中,例如 Javascript,C#,Python,Ruby 和 Go 等等。 Kotlin 协程基于已用于构建大型应用程序的既定概念。

在 Android 上,协程是解决两个问题的绝佳解决方案:

  1. 长时间运行的任务,是那些花费太长时间以至于阻塞主线程的任务
  2. 主安全性允许你确保可以从主线程调用所有挂起方法。

让我们深入了解每一个,看看协程如何帮助我们以更整洁的方式构建代码!

长时间运行的任务

获取网页或与API交互都涉及发出网络请求。同样,从数据库读取或从磁盘加载图像涉及读取文件。这些东西就是我称之为长时间运行的任务 - 任务花费太长时间让你的应用程序要停下来等待它们!

与网络请求相比,现代手机执行代码的速度可能很难理解。在 Pixel 2 上,单个 CPU 周期仅需0.0000004秒,这个数字在对人来说可能没什么概念。但是,如果你将网络请求视为一眨眼,大约400毫秒(0.4秒),则更容易理解 CPU 的运行速度。在一眨眼间,或者网络请求有点慢,CPU可以执行超过一百万次循环!

在 Android 上,每个应用程序都有一个主线程,负责处理UI(如绘制视图)和协调用户交互。如果此线程上发生了太多工作,则应用程序似乎挂起或减速,从而导致不良用户体验。任何长时间运行的任务都应该在不阻塞主线程的情况下完成,因此你的应用程序不会表现为所谓的“jank”,如卡顿的动画,或者对触摸事件响应缓慢。

为了从主线程执行网络请求,常见的模式是回调。回调提供了一个库的句柄,它可以用来在将来某个时候回调你的代码。使用回调,抓取 developer.android.com 可能看起来像这样:

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

即使从主线程调用 get ,它也将使用另一个线程来执行网络请求。然后,一旦从网络获得结果,回调将在主线程上被调用。这是处理长时间运行任务的好方法,而像 Retrofit 这样的库可以帮助你在不阻塞主线程的情况下发出网络请求。

使用协程执行长时间运行的任务

协程是一种简化用于管理长期运行任务(如fetchDocs)的代码的方法。为了探索协程如何使长时间运行的任务的代码更简单,让我们重写上面的回调示例以使用协同程序。

// 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 提供了一种执行此代码的方法,并且永远不会阻塞主线程。

协程通过添加两个新操作来构建常规功能。除了 invokereturn 之外,协程还添加了 suspendresume

  • suspend - 暂停当前协同程序的执行,保存所有局部变量
  • resume - 从暂停的地方继续暂停协同程序

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

suspend 和 resume 通力合作取代了回调。

在上面的示例中,get将在启动网络请求之前挂起协程。函数get仍将负责从主线程运行网络请求。然后,当网络请求完成时,它可以简单地恢复它挂起的协程,而不是调用回调来通知主线程。

aUNRzeZ.gif 展示kotlin如何实现 suspend 和 resume 来替换回调。

查看 fetchDocs 的执行方式,你可以看到 suspend 的工作原理。每当协程挂起时,都会复制并保存当前堆栈帧( Kotlin 用于跟踪正在运行的函数及其变量的位置)以供日后使用。恢复后,堆栈帧将从保存位置复制回来并再次开始运行。在动画的中间 - 当主线程上的所有协程都被挂起时,主线程可以自由更新屏幕并处理用户事件。 suspendresume 通力合作就能替换回调。很简约!

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

即使我们编写了看起来完全像阻塞网络请求的简单顺序代码,协程也会按照我们想要的方式运行我们的代码并避免阻塞主线程!

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

协程的主线程安全

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

但是,我们在 Android 应用程序中做了很多事情,这些事情在主线程上发生得太慢了。网络请求,解析JSON,从数据库读取或写入,甚至只是迭代大型列表。其中任何一个都有可能运行缓慢,导致用户可见的“jank”的任务应该在主线程之外运行。

使用 suspend 并不能告诉 Kotlin 在后台线程上运行函数。值得一提的是,并且通常协程将在主线程上运行。事实上,在响应UI事件时启动协程时使用Dispatchers.Main.immediate 是一个非常好的主意 - 这样,如果你没有做需要主线程安全的长期运行任务,结果就可以在用户的下一帧中可用。

协程将在主线程上运行,suspend 并不意味着后台线程。

要编写一个对主线程主安全来说太慢的函数,你可以告诉 Kotlin 协程在Default或IO调度程序上执行工作。在 Kotlin 中,所有协程必须在 dispatcher 中运行 - 即使它们在主线程上运行。协程可以自行挂起, dispatcher 知道如何恢复它们。

为了指定协程应该运行的位置,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 将自动提供主线程安全性。

与Kotlin协程一起使用时,Retrofit 和 Volley 等网络库管理自己的线程,并且在代码中不需要明确的主线程安全性。

要继续上面的示例,让我们使用调度器来定义get函数。在你的函数体内部调用withContext(Dispatchers.IO) 来创建一个将在IO调度器上运行的块。放在该块中的任何代码将始终在 IO 调度器上执行。由于 withContext 本身是一个 suspend 函数,因此它将使用协程来提供主线程的安全性。

// 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 来确保在包括Main在内的任何 Dispatcher 上调用每个函数都是安全的 - 这样调用者就不必考虑执行该函数所需的线程。

在此示例中,fetchDocs正在主线程上执行,但可以安全地调用get,后者在后台执行网络请求。因为协程支持挂起和恢复,所以只要 withContext 块完成,主线程上的协程就会恢复结果。

编写良好的挂起函数总是可以安全地从主线程调用。

让每个挂起功能都安全可靠是一个非常好的主意。如果它做任何触及磁盘,网络或甚至只是使用太多 CPU 的东西,请使用 withContext 使其从主线程调用安全。这是基于coroutines的库,如 Retrofit 和 Room 所遵循的。如果你在整个代码库中遵循此样式,则代码将更加简单,并避免将线程问题与应用程序逻辑混合在一起。一致地遵循协程,协程可以在主线程上自由启动,并使用简单的代码发出网络或数据库请求,同时保证用户不会看到“jank”。

withContext 的性能

在提供主线程安全的时候,withContext 与回调或 RxJava 一样快。在某些情况下,可以在回调的范围之外使用 withContext 进行优化。如果一个函数将对数据库进行10次调用,则可以告诉 Kotlin 在所有10个调用的外部 withContext 中切换一次。然后,即使数据库库将重复调用 withContext ,它仍将保留在同一个调度器中并遵循快速路径。此外, Dispatchers.DefaultDispatchers.IO 之间的切换已经过优化,以尽可能避免线程切换。

下一步是什么

在这篇文章中,我们探讨了协程在解决问题方面遇到的问题。协程是编程语言中一个非常古老的概念,由于它们能够使与网络交互的代码更简单,因此最近变得流行。

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

  1. 简化长时间运行任务的代码,例如从网络,磁盘读取,甚至解析巨大的JSON结果。
  2. 执行精确的主线程安全性,以确保你不会在不使难以读写代码的情况下意外阻塞主线程。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK