Kotlin 协程 使您能够编写简洁、简化的异步代码,使您的应用保持响应,同时管理长时间运行的任务,例如网络调用或磁盘操作。
本主题详细介绍了 Android 上的协程。如果您不熟悉协程,请务必阅读Android 上的 Kotlin 协程,然后再阅读本主题。
管理长时间运行的任务
协程以常规函数为基础,并添加了两个操作来处理长时间运行的任务。除了invoke
(或call
)和return
之外,协程还添加了suspend
和resume
suspend
暂停当前协程的执行,保存所有局部变量。resume
从暂停的地方继续执行已暂停的协程。
您只能从其他suspend
函数或使用协程构建器(如launch
)启动新协程来调用suspend
函数。
以下示例显示了假设长时间运行任务的简单协程实现
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()
调用,使其超越等效的基于回调的实现。例如,如果一个函数对网络进行了十次调用,您可以告诉Kotlin只切换一次线程,方法是使用一个外部的withContext()
。然后,即使网络库多次使用withContext()
,它也会停留在同一个调度器上,并避免切换线程。此外,Kotlin 会优化Dispatchers.Default
和Dispatchers.IO
之间的切换,以尽可能避免线程切换。
启动协程
您可以通过以下两种方式之一启动协程
通常,您应该从常规函数中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
元素将从包含范围继承。您可以通过将新的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)
}
}
}
其他协程资源
有关协程的更多资源,请参阅以下链接