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 主线程上运行协程。此调度程序只能用于与界面交互和执行轻量级工作。例如:调用
suspend函数、运行 Android 界面框架操作以及更新LiveData对象。 - Dispatchers.IO - 此调度程序专为在主线程之外执行磁盘或网络 I/O 而优化。例如:使用 Room 组件、读写文件以及运行任何网络操作。
- Dispatchers.Default - 此调度程序专为在主线程之外执行占用大量 CPU 资源的工作而优化。例如:对列表排序和解析 JSON。
继续上一个示例,您可以使用调度程序重新定义 get 函数。在 get 的主体内,调用 withContext(Dispatchers.IO) 来创建一个在 IO 线程池上运行的代码块。您放入该代码块中的任何代码始终通过 IO 调度程序执行。由于 withContext 本身是一个挂起函数,因此 get 函数也是一个挂起函数。
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() 来确保每个函数都是主线程安全 (main-safe) 的,这意味着您可以从主线程调用该函数。这样,调用者永远不需要考虑应该使用哪个线程来执行该函数。
在之前的示例中,fetchDocs() 在主线程上执行;但是,它可以安全地调用 get,后者在后台执行网络请求。由于协程支持 suspend 和 resume,当 withContext 代码块完成时,主线程上的协程会立即恢复并得到 get 的结果。
withContext() 的性能
withContext() 与等效的基于回调的实现相比,不会增加额外的开销。此外,在某些情况下,可以对 withContext() 调用进行优化,使其优于等效的基于回调的实现。例如,如果函数进行了十次网络调用,您可以通过使用外部 withContext() 告诉 Kotlin 只切换一次线程。这样,即使网络库多次使用 withContext(),它也会保持在同一个调度程序上,从而避免了线程切换。此外,Kotlin 会优化 Dispatchers.Default 和 Dispatchers.IO 之间的切换,以尽可能避免线程切换。
启动协程
您可以通过以下两种方式之一启动协程
launch启动一个新协程且不向调用者返回结果。任何被视为“发射并忘记 (fire and forget)”的工作都可以使用launch启动。async启动一个新协程,并允许您通过名为await的挂起函数返回结果。
通常,您应该从常规函数 launch 一个新协程,因为常规函数无法调用 await。仅在另一个协程内部,或者在挂起函数内部执行并行分解时,才使用 async。
并行分解
所有在 suspend 函数内部启动的协程都必须在该函数返回时停止,因此您可能需要确保这些协程在返回之前完成。利用 Kotlin 中的结构化并发,您可以定义一个启动一个或多个协程的 coroutineScope。然后,使用 await()(针对单个协程)或 awaitAll()(针对多个协程),您可以确保这些协程在函数返回之前完成。
作为一个示例,让我们定义一个异步获取两个文档的 coroutineScope。通过在每个延迟引用上调用 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 元素则从包含该协程的作用域继承而来。您可以通过向 launch 或 async 函数传递新的 CoroutineContext 来覆盖继承的元素。请注意,将 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)
}
}
}
更多协程资源
更多协程资源,请参考以下链接