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)
}
}
}
更多协程资源
有关更多协程资源,请参阅以下链接