Kotlin 协程可让您编写简洁、简化的异步代码,在管理网络调用或磁盘操作等耗时任务时,保持应用响应迅速。
本主题详细介绍了 Android 上的协程。如果您不熟悉协程,请务必在阅读本主题之前,先阅读Android 上的 Kotlin 协程。
管理耗时任务
协程通过添加两个操作来处理耗时任务,从而在常规函数的基础上进行构建。除了 invoke
(或 call
)和 return
之外,协程还添加了 suspend
和 resume
suspend
会暂停当前协程的执行,并保存所有局部变量。resume
会从暂停的位置继续执行已暂停的协程。
您只能从其他 suspend
函数中调用 suspend
函数,或者使用诸如 launch
等协程构建器启动新的协程。
以下示例展示了假设的耗时任务的简单协程实现
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
在此示例中,get()
仍然在主线程上运行,但在启动网络请求之前会暂停协程。当网络请求完成后,get
会恢复已暂停的协程,而不是使用回调来通知主线程。
Kotlin 使用堆栈帧来管理正在运行的函数以及所有局部变量。暂停协程时,当前的堆栈帧会被复制并保存以备后用。恢复时,堆栈帧会从保存的位置复制回来,函数会再次开始运行。尽管代码可能看起来像普通的顺序阻塞请求,但协程可确保网络请求不会阻塞主线程。
使用协程实现主线程安全
Kotlin 协程使用调度器来确定协程执行所使用的线程。要在主线程之外运行代码,您可以指示 Kotlin 协程在Default 或 IO 调度器上执行工作。在 Kotlin 中,所有协程都必须在调度器中运行,即使它们在主线程上运行也是如此。协程可以自行暂停,调度器负责恢复它们。
为指定协程的运行位置,Kotlin 提供了以下三个调度器供您使用
- Dispatchers.Main - 使用此调度器在 Android 主线程上运行协程。此调度器仅应用于与 UI 交互和执行快速工作。示例包括调用
suspend
函数、运行 Android UI 框架操作以及更新LiveData
对象。 - Dispatchers.IO - 此调度器经过优化,可在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、读写文件以及运行任何网络操作。
- Dispatchers.Default - 此调度器经过优化,可在主线程之外执行 CPU 密集型工作。示例用例包括对列表进行排序和解析 JSON。
继续上一个示例,您可以使用调度器重新定义 get
函数。在 get
的函数体内,调用 withContext(Dispatchers.IO)
以创建在 IO 线程池中运行的代码块。您放在该块中的任何代码都始终通过 IO
调度器执行。由于 withContext
本身是一个 suspend 函数,因此 get
函数也是一个 suspend 函数。
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 (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
借助协程,您可以精细地控制线程调度。由于 withContext()
可让您在不引入回调的情况下控制任何一行代码的线程池,因此您可以将其应用于非常小的函数,例如从数据库读取数据或执行网络请求。一个好的做法是使用 withContext()
来确保每个函数都是主线程安全的,这意味着您可以从主线程调用该函数。这样,调用方无需考虑应该使用哪个线程来执行该函数。
在前面的示例中,fetchDocs()
在主线程上执行;但是,它可以安全地调用 get
,后者会在后台执行网络请求。由于协程支持 suspend
和 resume
,因此一旦 withContext
块完成,主线程上的协程就会以 get
结果恢复。
withContext() 的性能
withContext()
与等效的基于回调的实现相比,不会增加额外的开销。此外,在某些情况下,可以对 withContext()
调用进行优化,使其超出等效的基于回调的实现。例如,如果一个函数对网络进行十次调用,您可以通过使用外部 withContext()
来告诉 Kotlin 只切换一次线程。然后,即使网络库多次使用 withContext()
,它也会停留在同一个调度器上并避免切换线程。此外,Kotlin 优化了 Dispatchers.Default
和 Dispatchers.IO
之间的切换,尽可能避免线程切换。
启动协程
您可以通过以下两种方式之一启动协程
launch
启动一个新的协程,并且不将结果返回给调用方。任何被认为是“即发即弃”的工作都可以使用launch
来启动。async
启动一个新的协程,并允许您通过一个名为await
的 suspend 函数返回结果。
通常,您应该从常规函数中 launch
一个新的协程,因为常规函数无法调用 await
。仅当在另一个协程内部或在 suspend 函数内部执行并行分解时才使用 async
。
并行分解
在 suspend
函数内启动的所有协程都必须在该函数返回时停止,因此您可能需要保证这些协程在返回之前完成。借助 Kotlin 中的结构化并发,您可以定义一个 coroutineScope
来启动一个或多个协程。然后,使用 await()
(用于单个协程)或 awaitAll()
(用于多个协程),您可以保证这些协程在函数返回之前完成。
举例来说,让我们定义一个 coroutineScope
来异步获取两个文档。通过在每个 deferred 引用上调用 await()
,我们保证两个 async
操作在返回一个值之前完成
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
您还可以在集合上使用 awaitAll()
,如以下示例所示
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
尽管 fetchTwoDocs()
使用 async
启动了新的协程,但该函数使用 awaitAll()
来等待这些已启动的协程完成,然后才返回。请注意,即使我们没有调用 awaitAll()
,coroutineScope
构建器也不会在所有新协程完成之前恢复调用 fetchTwoDocs
的协程。
此外,coroutineScope
会捕获协程抛出的任何异常,并将其路由回调用方。
有关并行分解的更多信息,请参阅组合挂起函数。
协程概念
CoroutineScope
一个 CoroutineScope
会跟踪它使用 launch
或 async
创建的任何协程。正在进行的工作(即正在运行的协程)可以通过随时调用 scope.cancel()
来取消。在 Android 中,一些 KTX 库为特定的生命周期类提供了自己的 CoroutineScope
。例如,ViewModel
有一个 viewModelScope
,而 Lifecycle
有 lifecycleScope
。然而,与调度器不同的是,CoroutineScope
不会运行协程。
viewModelScope
也用于Android 上的协程后台线程处理中的示例。但是,如果您需要创建自己的 CoroutineScope
来控制应用特定层中协程的生命周期,可以按如下方式创建:
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
已取消的作用域无法创建更多协程。因此,您应该仅在控制其生命周期的类被销毁时才调用 scope.cancel()
。使用 viewModelScope
时,ViewModel
类会在 ViewModel 的 onCleared()
方法中自动为您取消作用域。
Job
一个 Job
是协程的句柄。您使用 launch
或 async
创建的每个协程都会返回一个 Job
实例,该实例唯一标识协程并管理其生命周期。您还可以将 Job
传递给 CoroutineScope
以进一步管理其生命周期,如以下示例所示
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
CoroutineContext
一个 CoroutineContext
使用以下元素集定义协程的行为
Job
: 控制协程的生命周期。CoroutineDispatcher
: 将工作分派到适当的线程。CoroutineName
: 协程的名称,对调试很有用。CoroutineExceptionHandler
: 处理未捕获的异常。
对于在作用域内创建的新协程,一个新的 Job
实例会被分配给新协程,而其他 CoroutineContext
元素则继承自包含作用域。您可以通过将新的 CoroutineContext
传递给 launch
或 async
函数来覆盖继承的元素。请注意,将 Job
传递给 launch或
async
没有效果,因为新的协程总是会分配一个新的 Job
实例。
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
更多协程资源
有关更多协程资源,请参阅以下链接