原文连接:Coroutines on Android (part I): Getting the backgroundhtml
原文做者:Sean McQuillanandroid
这是「怎样在 Android 上使用协程」的系列文章的第一篇。git
这篇内容关注协程怎么工做的以及它们解决什么问题。github
Kotlin 的协程采用了一种新的并发方式(a new style of concurrency),能够在 Android 上简化异步代码。数据库
虽然在 Kotlin 1.3 协程做为全新特性出现的,可是协程的概念从编程语言诞生之初就已经存在了。第一个探索使用协程的语言是的 Simula ,出如今 1967年。编程
最近几年,协程愈来愈受欢迎,如今许多流行的编程语言里都有协程,如 Javascript、C#、Python、Ruby等等。Kotlin 协程的设计基于它们这些构建过大型应用的经验。安全
在 Android 上,协程能够很是好的解决两个问题:网络
让咱们深刻这两个问题,看看协程是如何帮助咱们写出更简洁的代码。并发
访问网页或和 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。
Kotlin 经过函数上的 suspend 关键字来添加这个功能。你只能从其余挂起函数调用挂起函数,或者使用协程启动器相似 launch
来启动一个新的协程。
挂起配合上恢复代替回调
Suspend and resume work together to replace callbacks.
在上面的例子中,get
会在启动网络请求以前被 挂起 。而后get
函数会脱离主线程继续负责运行网络请求。而后,当网络请求完成时,它不用回调来通知主线程,而是简单的 恢复 挂起的协程。
查看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
与使用回调或者 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 上,你可使用它们解决两个很是常见的问题:
在下一篇文章,咱们探索它们是如何配合 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:)。