原文做者 :Sean McQuillanjavascript
原文地址: Coroutines on Android (part I): Getting the backgroundhtml
译者 : 秉心说java
这是关于在 Android 中使用协程的一系列文章。本篇让咱们先来看看协程是如何工做的以及它解决了什么问题。python
Kotlin 的 Coroutines (协程) 带来了一种新的并发方式,在 Android 上,它能够用来简化异步代码。尽管 Kotlin 1.3 才带来稳定版的协程,可是自编程语言诞生以来,协程的概念就已经出现了。第一个使用协程的语言是发布于 1967 年的 Simula 。android
在过去的几年中,协程变得愈来愈流行。如今许多流行的编程语言都加入了协程,例如 Javascript , C# , Python , Ruby , Go 等等。Kotlin 协程基于以往构建大型应用中已创建的一些概念。git
在安卓中,协程很好的解决了两个问题:github
下面让咱们深刻了解协程如何帮助咱们构建更干净的代码!golang
获取网页,和 API 进行交互,都涉及到了网络请求。一样的,从数据库读取数据,从硬盘中加载图片,都涉及到了文件读取。这些就是咱们所说的耗时任务,App 不可能特意暂停下来等待它们执行完成。数据库
和网络请求相比,很难具体的想象现代智能手机执行代码的速度有多快。Pixel 2
的一个 CPU 时钟周期不超过 0.0000000004
秒,这是一个对人类来讲很难理解的一个数字。可是若是你把一次网络请求的耗时想象成一次眨眼,大概 0.4 s,这就很好理解 CPU 执行的到底有多快了。在一次眨眼的时间内,或者一次较慢的网络请求,CPU 能够执行超过一百万次时钟周期。编程
在 Android 中,每一个 app 都有一个主线程,负责处理 UI(例如 View 的绘制)和用户交互。若是在主线程中处理过多任务,应用将会变得卡顿,随之带来了很差的用户体验。任何耗时任务都不该该阻塞主线程,
为了不在主线程中进行网络请求,一种通用的模式是使用 CallBack
(回调),它能够在未来的某一时间段回调进入你的代码。使用回调访问 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 协程提供了一种永远不会阻塞主线程的代码执行方式。
协程添加了两个操做来构建一些常规功能。除了 invoke(or call)
和 return
,它额外添加了 suspend(挂起)
和 resume(恢复)
。
在 Kotlin 中,经过给函数添加 suspend
关键字来实现此功能。你只能在挂起函数中调用挂起函数,或者经过协程构造器,例如 launch
,来开启一个新的协程。
挂起和恢复共同工做来替代回调。
在上面的例子中,get()
方法在进行网络请求以前会挂起协程,它也负责进行网络请求。而后,当网络请求结束时,它仅仅只须要恢复以前挂起的协程,而不是调用回调函数来通知主线程。
看一下 fetchDocs
是如何执行的,你就会明白 suspend 是如何工做的了。不管一个协程什么时候被挂起,它的当前栈帧(用来追踪正在运行的函数及其变量)将被复制并保存。当进行 resume 时,栈帧将从以前被保存的地方复制回来并从新运行。在上面动画的中间部分,当主线程上的全部协程都被挂起,就有时间去更新 UI,处理用户事件。总之,挂起和恢复替代了回调,至关的整洁!
当主线程上的全部协程都被挂起,它就有时间作其余事情了。
即便咱们直接顺序书写代码,看起来就像是会致使阻塞的网络请求同样,可是协程会按咱们所但愿的那样执行,不会阻塞主线程。
下面,让咱们看看协程是如何作到主线程安全的,而且探索一下 disaptchers(调度器)
。
在 Kotlin 协程中,编写良好的挂起函数在主线程中调用老是安全的。不管挂起函数作了什么,老是应该容许任何线程调用它们。
可是,在 Android 应用中,咱们若是把不少工做都放在主线程作会致使 APP 运行缓慢,例如网络请求,JSON 解析,读写数据库,甚至是大集合的遍历。它们中任何一个都会致使应用卡顿,下降用户体验。因此它们不该该运行在主线程。
使用 suspend 并不意味着告诉 Kotlin 必定要在后台线程运行函数。值得一提的是,协程常常运行在主线程。事实上,当启动一个用于响应用户事件的协程时,使用 Dispatchers.Main.immediate 是一个好主意。
协程也会运行在主线程,suspend 并不必定意味着后台运行。
为了让一个函数不会使主线程变慢,咱们能够告诉 Kotlin 协程使用 Default
或者 IO
调度器。在 Kotlin 中,全部的协程都须要使用调度器,即便它们运行在主线程。协程能够挂起本身,而调度器就是用来告诉它们如何恢复运行的。
为了指定协程在哪里运行,Kotlin 提供了 Dispatchers 来处理线程调度。
+-----------------------------------+
| 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 |
+-----------------------------------+
复制代码
继续上面的例子,让咱们使用调度器来定义 get 函数。在 get 函数的方法体内使用 withContext(Dispatchers.IO)
定义一段代码块,这个代码块将在调度器 Dispatchers.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
复制代码
经过协程,你能够细粒度的控制线程调度,由于 withContext
让你能够控制任意一行代码运行在什么线程上,而不用引入回调来获取结果。你可将其应用在很小的函数中,例如数据库操做和网络请求。因此,比较好的作法是,使用 withContext
确保每一个函数在任意调度器上执行都是安全的,包括 Main
,这样调用者在调用函数时就不须要考虑应该运行在什么线程上。
编写良好的挂起函数被任意线程调用都应该是安全的。
保证每一个挂起函数主线程安全无疑是个好主意,若是它设计到任何磁盘,网络,或者 CPU 密集型的任务,请使用 withContext 来确保主线程调用是安全的。这也是基于协程的库所遵循的设计模式。若是你的整个代码库都遵循这一原则,你的代码将会变得更加简单,线程问题和程序逻辑也不会再混在一块儿。协程能够自由的从主线程启动,数据库和网络请求的代码会更简单,且能保证用户体验。
对于提供主线程安全性,withContext 与回调或 RxJava 同样快。在某些状况下,甚至可使用协程上下文 withContext 来优化回调。若是一个函数将对数据库进行10次调用,那么您能够告诉 Kotlin 在外部的 withContext 中调用一次切换。尽管数据库会重复调用 withContext ,可是他它将在同一个调度器下,寻找最快路径。此外,Dispatchers.Default
和 Dispatchers.IO
之间的协程切换已通过优化,以尽量避免线程切换。
在这篇文章中咱们探索了协程解决了什么问题。协程是编程语言中一个很是古老的概念,因为它们可以使与网络交互的代码更简单,所以最近变得更加流行。
在安卓上,你可使用协程解决两个常见问题:
在下一篇文章中,咱们将探索它们是如何适应 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! )
下一篇:在 Android 上使用协程(二):Getting started
译者说: 自我感受翻译的有点灾难,不过灾难也得翻译下去,权当学习英语了!
文章首发微信公众号:
秉心说
, 专一 Java 、 Android 原创知识分享,LeetCode 题解。更多 JDK 源码解析,扫码关注我吧!