借助Kotlin协程,可以编写整洁,简化的异步代码,用来管理长时间运行的任务(例如网络调用或磁盘操作),使应用保持快速响应。同时管理长时间运行的任务(例如网络调用或磁盘操作)。
本文详细介绍协程的高级概念。如果不熟悉协程,可先阅读Android上的协程:简介,然后再阅读本文。
管理耗时任务
协程在常规函数的基础上添加了两项操作,用于处理耗时任务。在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
本身就是一个挂起函数,因此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()
来确保每个函数都是主线程安全的。这意味着,可以从主线程调用任意函数。这样,调用方就不需要考虑具体应用使用哪个线程来执行函数了。
在前面的示例中,fetchDocs()
虽在主线程上执行;它也可以安全地调用有get
,还是会在后台执行网络请求。由于协程支持suspend和resume,因此withContext
代码块完成后,主线程上的协程会立即根据get
的结果恢复至原来的线程。
重要提示:使用
suspend
不会让 Kotlin 在后台线程上运行函数。suspend
函数在主线程上运行是一种正常的现象。在主线程上启动协程的情况也很常见。当您需要确保主线程安全时(例如,从磁盘上读取数据或向磁盘中写入数据、执行网络操作或运行占用大量 CPU 资源的操作时),应始终在suspend
函数内使用withContext()
。
withContext()
的作用
与基于回调的等效实现相比,withContext()不会增加额外的开销。此外,在某些情况下,还可以优化withContext()
调用,使其超越基于加调的等效实现。例如,如果某个函数对一个网络进行十次调用,可以使用外部withContext()
让Kotlin只切换一次线程。此外,Kotlin还优化了Dispatchers.Default
与Dispathcers.IO
之间的切换,以尽可能避免线程切换。
重要提示:利用一个使用线程池的调度程序(例如
Dispatchers.IO
或Dispatchers.Default
)不能保证块在同一线程上从上到下执行。在某些情况下,Kotlin 协程在suspend
和resume
后可能会将执行工作移交给另一个线程。这意味着,对于整个withContext()
块,线程局部变量可能并不指向同一个值。
启动协程
可以通过以下两种方式来启动协程:
通常,应使用launch
来启动常规的新协程,只有在另一个协程内或在挂起函数内且在执行并行分解时才使用async
。
警告:
launch
和async
处理异常的方式不同。由于async
希望在某一时刻对await
进行最终调用,因此它持有异常并将其作为await
调用的一部分重新抛出。这意味着,如果您使用async
从常规函数启动新协程,则能以静默方式丢弃异常。这些丢弃的异常不会出现在崩溃指标中,也不会在 logcat 中注明。如需了解详情,请参阅协程中的取消和异常。
并行分解
在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
}
值得注意的是,即使没有调用awaitAll()
,coroutineScope
构建器也会等到所有 新协程都完成后才恢复名为fetchTwoDocs
的协程。
此外,coroutineScope
会捕获协程抛出的所有异常,并将其传送加调用方。
如需详细了解并行分解,可以阅读编写挂起函数
协程概念
CoroutineScope
CorutineScope会跟踪使用launch
或 async
创建的任何协程。可以随时调用 scope.cancel()
来取消正在运行的协程。在Android中,某些KTX库为某些生命期提供自己的 CoroutineScope
。 例如,ViewModel
有 viewModelScope
,Lifecycle
有 lifecylceScope
。不过,与高度程序不同,CoroutineScope
不运行协程。
注意:如需详细了解
viewModelScope
,请参阅 Android 中的简易协程:viewModelScope。
viewModelScope
也可用于 Android 上采用协程的后台线程中的示例内。但是,如果您需要创建自己的 CoroutineScope
以控制协程在应用的特定层中的生命周期,则可以创建一个如下所示的 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
无法再创建协程。因此,公当控制其生命周期的类被销毁时,才应调用 scope.cancel()
。使用 viewModelScope
时,ViewModel
类会在 ViewModel
的 onCleared()
方法中自动取消 scope
。
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)
}
}
}
注意 :如需详细了解
CoroutineExceptionHandler
,请参阅协程博文中的异常