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

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

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

协程解决什么问题

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 如何实现 挂起 和 恢复来替换回调的动画。

查看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 在一个后台线程挂起。值的一提的说,协程一般在主线程运行。实际上,在响应一个 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 容许你控制在什么线程上执行任何代码块,而不须要引入 callback 来返回结果,因此你能够将它应用于很是小的函数,好比从数据库读取数据或执行网络请求。所以,一个好的实践是使用 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

相关文章
相关标签/搜索