Kotlin协程引入了一种新的并发方式,可以在 Android 上使用它来简化异步代码。虽然协程在 1.3 中是 Kotlin 的新概念,但协程的概念自编程语言诞生以来就一直存在。第一个探索使用协程的语言是1967 年的Simula。
在过去的几年中,协程越来越受欢迎,现在已包含在许多流行的编程语言中,例如Javascript、C#、Python、Ruby和Go等等。Kotlin 协程基于已用于构建大型应用程序的既定概念。
在 Android 上,协程可以很好地解决两个问题:
长时间运行的任务是花费太长时间阻塞主线程的任务。
Main-safety允许您确保可以从主线程调用任何挂起函数。
让我们深入了解协程,看看协程如何帮助我们以更简洁的方式构建代码!
获取网页或与 API 交互都涉及发出网络请求。类似地,从数据库读取或从磁盘加载图像涉及读取文件。这些类型的事情就是我所说的长时间运行的任务——这些任务花费的时间太长以至于您的应用程序无法停止并等待它们!
与网络请求相比,现代手机执行代码的速度可能很难理解。在 Pixel 2 上,单个 CPU 周期仅需不到 0.0000000004 秒,这个数字很难用人类的术语来理解。但是,如果您将网络请求视为一眨眼,大约 400 毫秒(0.4 秒),就更容易理解 CPU 的运行速度。一眨眼的功夫,或者一个有点慢的网络请求,CPU 可以执行超过 10 亿个周期!
在 Android 上,每个应用程序都有一个主线程,负责处理 UI(如绘图视图)和协调用户交互。如果此线程上发生过多工作,应用程序似乎会挂起或变慢,从而导致不良的用户体验。任何长时间运行的任务都应该在不阻塞主线程的情况下完成,这样您的应用程序就不会显示所谓的“卡顿”,如冻结的动画,或者对触摸事件的响应缓慢。
为了在主线程之外执行网络请求,一个常见的模式是回调。回调提供了一个库的句柄,它可以用来在将来某个时间回调到您的代码中。使用回调的话,获取developer.android.com
上的数据可能是下面的样子:
class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}
即使get
是从主线程调用,它也会使用另一个线程来执行网络请求。然后,一旦网络上的结果可用,就会在主线程上调用回调。这是处理长时间运行任务的好方法,像Retrofit
这样的库可以帮助您在不阻塞主线程的情况下发出网络请求。
协程是一种简化用于管理长时间运行任务(如fetchDocs. 为了探索协程如何使长时间运行的任务的代码更简单,让我们重写上面的回调示例以使用协程。
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
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(或 call)和return之外,协程还添加了suspend
和resume
。
Suspend 和 Resume 一起工作来代替回调。
在上面的示例中,get将在启动网络请求之前暂停协程。该函数get仍将负责在主线程之外运行网络请求。然后,当网络请求完成时,它可以简单地恢复它挂起的协程,而不是调用回调来通知主线程。
查看如何fetchDocs执行,您可以了解suspend
的工作原理。每当协程挂起时,都会复制并保存当前堆栈帧(Kotlin 用来跟踪正在运行的函数及其变量的位置)。当它恢复时,堆栈帧从它保存的地方被复制回来并再次开始运行。在动画中间——当主线程上的所有协程都挂起时,主线程可以自由地更新屏幕和处理用户事件。suspend 和 resume 一起替换回调。很简约!
当主线程上的所有协程都挂起后,主线程就可以自由地做其他工作了。
即使我们编写了看起来完全像阻塞网络请求的简单顺序代码,协程也会按照我们的需要运行我们的代码并避免阻塞主线程!
接下来,让我们看看如何使用协程来实现主安全并探索调度程序。
在 Kotlin 协程中,编写良好的挂起函数始终可以安全地从主线程调用。不管他们做什么,他们应该总是允许任何线程调用他们。
但是,我们在 Android 应用程序中做的很多事情都太慢而无法在主线程上发生。网络请求、解析 JSON、从数据库读取或写入,甚至只是遍历大型列表。其中任何一个都有可能运行缓慢到足以导致用户可见的“卡顿”,并且应该在主线程之外运行。
使用suspend
不会告诉 Kotlin 在后台线程上运行函数。值得清楚且经常说的是协程将在主线程上运行。事实上,在启动协程以响应 UI 事件时使用Dispatchers.Main.immediate
是一个非常好的主意——这样,如果您最终没有执行需要 main-safety 的长时间运行的任务,结果可以在下一帧中为用户提供。
协程会运行在主线程上,挂起不代表后台。
Default要使一个对于主线程来说工作速度太慢的函数是主安全的,您可以告诉 Kotlin 协程在 the或dispatcher上执行工作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.Main
withContext(Dispatchers.IO) {
// Dispatchers.IO
/* perform blocking network IO here */
}
// Dispatchers.Main
使用协程,您可以使用细粒度控制进行线程分派。因为withContext
允许您控制任何代码行在哪个线程上执行而无需引入回调来返回结果,所以您可以将它应用于非常小的功能,例如从数据库读取或执行网络请求。因此,一个好的做法是withContext
确保每个函数都可以安全地在任何Dispatcher包含上调用Main——这样调用者就不必考虑执行该函数需要什么线程。
在这个例子中,fetchDocs
在主线程上执行,但可以安全地调用get它在后台执行网络请求。因为协程支持suspend
和resume
,一旦块withContext
完成,主线程上的协程将恢复结果。
编写良好的挂起函数总是可以安全地从主线程(或主线程)调用。
使每个挂起函数都是主线程安全的是一个非常好的主意。如果它做任何涉及磁盘、网络的事情,甚至只是使用过多的 CPU,请使用withContext它来确保从主线程调用是安全的。这是 Retrofit 和 Room 等基于协程的库所遵循的模式。如果您在整个代码库中都遵循这种风格,您的代码将会更加简单,并且可以避免将线程问题与应用程序逻辑混在一起。如果始终遵循,协程可以在主线程上自由启动并使用简单的代码发出网络或数据库请求,同时保证用户不会看到“卡顿”。
withContext
与提供主要安全性的回调或 RxJava 一样快。在某些情况下,甚至可以优化withContext
调用,使其超出回调所能达到的范围。如果一个函数将对数据库进行 10 次调用,您可以告诉 Kotlin 在所有 10 次调用周围切换一次withContext
。然后,即使数据库 library 会对withContext
重复调用,它也会留在同一个调度程序上并遵循快速路径。此外,对Dispatchers.Default
和之间的切换Dispatchers.IO
进行了优化,以尽可能避免线程切换。
在这篇文章中,我们探讨了协程最擅长解决的问题。协程是编程语言中一个非常古老的概念,最近变得流行,因为它们能够简化与网络交互的代码。
在 Android 上,您可以使用它们来解决两个非常常见的问题:
1.简化长时间运行任务的代码,例如从网络、磁盘读取,甚至解析大型 JSON 结果。
2.执行精确的 main-safety 以确保您不会意外阻塞主线程而不会使代码难以读写。