当使用kotlin协程开发安卓时,如何高效组织异步任务是很重要的,常见的两个coroutine scope为SupervisorScope
和ViewModelScope
,虽然两个都是用来管理协程,它们的行为和用例却存在显著差异,本篇文章将会讨论其中的差异,重点讨论两者如何处理异常、管理子协程以及为什么为每个task选择正确的coroutine scope对于构建高效、高容错的应用程序至关重要。
用途和用意
SupervisorScope
SupervisorScope
被设计用于管理多个独立的协程,即使其中一个失败了也不会影响其它协程,此Scope在以下情况下非常有用:您希望运行多个并发任务,但不希望一个任务的失败导致其他任务被取消。比如你的app需要在某个地方发起多个网络请求,且是不同API,某个请求的失败不会影响其它请求。
supervisorScope {
launch {
// Task 1: e.g., fetching user data
}
launch {
// Task 2: e.g., fetching posts
throw Exception("This will only cancel Task 2")
}
// Task 1 will continue even if Task 2 fails
}
SupervisorScope
的一个常用场景就是当需要管理多个无强相关的任务的时候,比如发起不同且独立的网络请求或者并行进行数据拉取。
ViewModelScope
ViewModelScope
则不同,它是一个ViewModel
生命周期感知的scope,当ViewModel
被cleared(如UI销毁)的时候,viewmodelScope
内的协程都会自动取消,这个Scope对于UI绑定的异步操作是最理想的选择,即当用户切到关闭当前页面的时候需要停止所有异步任务的场景。
viewModelScope.launch {
// Fetch data for the UI
}
- 如果你的UI依赖于数据获取或者复杂计算结果,使用
viewModelScope
可以确保当不需要的时候这些任务都可以自动取消,有助于节约资源并防止内存泄漏。
取消策略
SupervisorScope
-
当一个子协程抛出异常,它不会取消其它兄弟协程,这跟普通的coroutine scope不同,效果正好相反,此策略对于不受其他任务失败影响的操作非常有用。
-
但是,如果
SupervisorScope
本身被取消,所有的子协程也会被无视其当前状态而取消,因为每个兄弟协程都是独立的,一个的失败并不会发生异常传播。
viewModelScope
-
取消策略跟
ViewModel
的生命周期绑定,如果ViewModel
被cleared,全部的协程都会被自动取消,防止内存泄漏 -
跟
SupervisorScope
一样,ViewModelScope
本身并不阻止兄弟协程的取消,如果其中一个协程失败了,它通常会显式地传播失败。 -
viewModelScope
是使用默认job层次结构创建的,这意味着它类似于结构化scope,因为如果任何兄弟协程发生故障,它会自动取消所有协程。
实践
SupervisorScope
- kotlin中可以使用
supervisorScope
函数来创建SupervisorScope
,它默认不属于任何lifecycle组件,所以它经常在自定义协程构造器或者生命周期观察者中使用。
supervisorScope {
// launch multiple independent child coroutines here
}
viewModelScope
viewModelScope
在引入了lifecycle-viewmodel-ktx
库后,就会自动在任意viewModel
中可用,简单调用viewModelScope.launch
就可以在该scope开启一个协程。
viewModelScope.launch {
// perform ViewModel-related tasks here
}
-
当使用
viewModelScope
,你需要防止一个协程的失败导致其它协程被取消,即需要明确地处理错误,可以用try-catch
包住协程内的代码或者使用CoroutineExceptionHandler
来优雅地处理异常。 -
如果不明确地做异常处理,由于使用的是结构化并发模型,一个协程的失败会导致其它兄弟协程的取消。
// Using viewModelScope
viewModelScope.launch {
launch {
// Task 1
}
launch {
// Task 2
throw Exception("Task 2 failed") // This will cancel Task 1 as well
}
}
// Using SupervisorScope
viewModelScope.launch {
supervisorScope {
launch {
// Task 1
}
launch {
// Task 2
throw Exception("Task 2 failed") // Task 1 will continue running
}
}
}
如何选择
SupervisorScope
- 你需要互不影响的独立协程
- 任务低耦合且并行运行,生命周期无关,如repository中做并行数据拉取
viewModelScope
- UI相关任务,生命周期感知
- 任务跟某个页面相关,页面销毁时释放
UncaughtExceptionHandler
现在聊聊UncaughtExceptionHandler
,它跟协程的异常如何被处理有关,特别是在viewModelScope
或者SupervisorScope
这样的中。
UncaughtExceptionHandler基础
- 在Java和Kotlin中,
UncaughtExceptionHandler
是一个线程中捕获异常的handler,在协程中,未处理的异常会根据协程的上下文、作用域和父子关系进行不同的管理。
Uncaught Exceptioin in viewModelScope
-
viewModelScope
使用结构化并发,所有的协程都会链接到viewModel
,如果有一个协程发生异常且未捕获(未try-catch),该异常会传播到scope的父scope,即viewModelScope
本身,也即意味着默认所有兄弟协程会被取消,除非每个协程明确做了异常处理。 -
如果异常未被捕获,它会往上冒到默认的
CoroutineExceptionHandler
,然而,由于viewModelScope
是生命周期感知的,未捕获异常会导致scope通过取消所有协程来清理作用域,这使得显示的CoroutineExceptionHandler
特别有用。
SupervisorScope和异常处理
-
SupervisorScope
允许各个协程独立运行,这一策略是通过SupervisorJob
来控制,它基本上重写了结构化并发模型中级联取消的常见行为。 -
有了
SupervisorScope
,如果你添加一个UncaughtExceptionHandler
(通过CoroutineExceptionHandler
),就可以实现不影响兄弟协程而管理异常,使之对于可以预测到失败的任务很有用。
CoroutineExceptionHandler搭配viewModelScope和SupervisorScope使用
CoroutineExceptionHandler
可以在viewModelScope
和SupervisorScope
中attach到独立的协程以实现优雅地异常捕获,比如,当协程失败时可以打印日志或者执行特定的操作而不会导致整个作用域的取消。
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.e("Error", "Caught exception: ${throwable.message}")
}
viewModelScope.launch(exceptionHandler) {
// Coroutine with exception handler
throw Exception("Test exception")
}
上面的例子中,如果发生异常,会被exceptionHandler
捕获而不会传播异常到viewModelScope
特性 | SupervisorScope | viewModelScope |
---|---|---|
生命周期感知 | 无感知 | 生命周期感知(跟ViewModle) |
失败处理 | 独立失败;不影响兄弟协程 | 传播失败;取消全部(除非单独处理) |
理想用例 | 独立;并发操作 | UI绑定;生命周期绑定操作 |
取消 | scope本身取消才会取消所有子协程 | ViewModel被clear时取消所有协程 |
总结
SupervisorScope
提供了独立的失败处理,使其适用于并发操作,其中一个操作中的失败不应影响其他操作。另一方面,viewModelScope
非常适合管理与ViewModel
生命周期相关的协程,特别是对于与UI相关的任务,当页面销毁时需要停止。
UncaughtExceptionHandler
(通过CoroutineExceptionHandler
)在管理协程中未处理的异常方面起着至关重要的作用。在viewModelScope
中,未处理的异常通常会取消整个作用域,但通过添加一个CoroutineExceptionHandler
,你可以在不取消所有协程的情况下管理错误。在SupervisorScope
中,这种隔离更加直接,因为兄弟协程的设计不会在失败时影响彼此的执行。