协程中的取消和异常 | 驻留任务详解
lipiwang 2024-11-01 14:10 7 浏览 0 评论
在本系列第二篇文章 协程中的取消和异常 | 取消操作详解 中,我们学到,当一个任务不再被需要时,正确地退出十分的重要。在 Android 中,您可以使用 Jetpack 提供的两个 CoroutineScopes: viewModelScope 和 lifecycleScope,它们可以在 Activity、Fragment、Lifecycle 完成时退出正在运行的任务。如果您正在创建自己的 CoroutineScope,记得将它绑定到某个任务中,并在需要的时候取消它。
然而,在有些情况下,您会希望即使用户离开了当前界面,操作依然能够执行完成。因此,您就不会希望任务被取消,例如,向数据库写入数据或者向您的服务器发送特定类型的请求。
下面我们就来介绍实现此类情况的模式。
协程还是 WorkManager?
协程会在您的应用进程活动期间执行。如果您需要执行一个能够在应用进程之外活跃的操作 (比如向远程服务器发送日志),在 Android 平台上建议使用 WorkManager。WorkManager 是一个扩展库,用于那些预期会在将来的某个时间点执行的重要操作。
请针对那些在当前进程中有效的操作使用协程,同时保证可以在用户关闭应用时取消操作 (例如,进行一个您希望缓存的网络请求)。那么,实现这类操作的最佳实践是什么呢?
协程的最佳实践
由于本文所介绍的模式是在协程的其它最佳实践的基础之上实现的,我们可以借此机会回顾一下:
1. 将调度器注入到类中
不要在创建协程或调用 withContext 时硬编码调度器。
? 好处: 便于测试。您可以在进行单元测试或仪器测试时轻松替换掉它们。
2. 应当在 ViewModel 或 Presenter 层创建协程
如果是仅与 UI 相关的操作,则可以在 UI 层执行。如果您认为这条最佳实践在您的工程中不可行,则很有可能是您没有遵循第一条最佳实践 (测试没有注入调度器的 ViewModel 会变得更加困难;这种情况下,暴露出挂起函数会使测试变得可行)。
? 好处: UI 层应该尽量简洁,并且不直接触发任何业务逻辑。作为代替,应当将响应能力转移到 ViewModel 或 Presenter 层实现。在 Android 中,测试 UI 层需要执行插桩测试,而执行插桩测试需要运行一个模拟器。
3. ViewModel 或 Presenter 以下的层级,应当暴露挂起函数与 Flow
如果您需要创建协程,请使用 coroutineScope 或 supervisorScope。而如果您想要将协程限定在其他作用域,请继续阅读,接下来本文将对此进行讨论。
? 好处: 调用者 (通常是 ViewModel 层) 可以控制这些层级中任务的执行和生命周期,也可以在需要时取消这些任务。
协程中那些不应当被取消的操作
假设我们的应用中有一个 ViewModel 和一个 Repository,它们的相关逻辑如下:
class MyViewModel(private val repo: Repository) : ViewModel() {
fun callRepo() {
viewModelScope.launch {
repo.doWork()
}
}
}
class Repository(private val ioDispatcher: CoroutineDispatcher) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
veryImportantOperation() // 它不应当被取消
}
}
}
我们不希望用 viewModelScope 来控制 veryImportantOperation(),因为 viewModelScope 随时都可能被取消。我们想要此操作的运行时长超过 viewModelScope,这个目的要如何达成呢?
我们需要在 Application 类中创建自己的作用域,并在由它启动的协程中调用这些操作。这个作用域应当被注入到那些需要它的类中。
与稍后将在本文中看到的其他解决方案 (如 GlobalScope) 相比,创建自己的 CoroutineScope 的好处是您可以根据自己的想法对其进行配置。无论您是需要 CoroutineExceptionHandler,还是想使用自己的线程池作为调度器,这些常见的配置都可以放在自己的 CoroutineScope 的 CoroutineContext 中。
您可以称其为 applicationScope。applicationScope 必须包含一个 SupervisorJob(),这样协程中的故障便不会在层级间传播 (见本系列第三篇文章: 协程中的取消和异常 | 异常处理详解):
class MyApplication : Application() {
// 不需要取消这个作用域,因为它会随着进程结束而结束
val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)
}
由于我们希望它在应用进程存活期间始终保持活动状态,所以我们不需要取消 applicationScope,进而也不需要保持 SupervisorJob 的引用。当协程所需的生存期比调用处作用域的生存期更长时,我们可以使用 applicationScope 来运行协程。
从 application CoroutineScope 创建的协程中调用那些不应当被取消的操作
每当您创建一个新的 Repository 实例时,请传入上面创建的 applicationScope。对于测试,可以参考后文的 Testing 部分。
应该使用哪种协程构造器?
您需要基于 veryImportantOperation 的行为来使用 launch 或 async 启动新的协程:
- 如果需要返回结果,请使用 async 并调用 await 来等待其完成;
- 如果不是,请使用 launch 并调用 join 来等待其完成。请注意,如 本系列第三部分所述,您必须在 launch 块内部手动处理异常。
下面是使用 launch 启动协程的方式:
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
externalScope.launch {
//如果这里会抛出异常,那么要将其包裹进 try/catch 中;
//或者依赖 externalScope 的 CoroutineScope 中的 CoroutineExceptionHandler
veryImportantOperation()
}.join()
}
}
}
或使用 async:
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork(): Any { // 在结果中使用特定类型
withContext(ioDispatcher) {
doSomeOtherWork()
return externalScope.async {
// 异常会在调用 await 时暴露,它们会在调用了 doWork 的协程中传播。
// 注意,如果正在调用的上下文被取消,那么异常将会被忽略。
veryImportantOperation()
}.await()
}
}
}
在任何情况下,都无需改动上面的 ViewModel 的代码。就算 ViewModelScope 被销毁,使用 externalScope 的任务也会持续运行。就像其他挂起函数一样,只有在 veryImportantOperation() 完成之后,doWork() 才会返回。
有没有更简单的解决方案呢?
另一种可以在一些用例中使用的方案 (可能是任何人都会首先想到的方案),便是将 veryImportantOperation 像下面这样用 withContext 封装进 externalScope 的上下文中:
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
withContext(externalScope.coroutineContext) {
veryImportantOperation()
}
}
}
}
但是,此方法有下面几个注意事项,使用的时候需要注意:
- 如果调用 doWork() 的协程在 veryImportantOperation 开始执行时被退出,它将继续执行直到下一个退出节点,而不是在 veryImportantOperation 结束后退出;
- CoroutineExceptionHandler 不会如您预期般工作,这是因为在 withContext 中使用上下文时,异常会被重新抛出。
测试
由于我们可能需要同时注入调度器和 CoroutineScop,那么这些场景里分别需要注入什么呢?
测试时要注入什么
测试时要注入什么
说明文档:
- TestCoroutineDispatcher
- MainCoroutineRule
- TestCoroutineScope
- AsyncTask.THREAD_POOL_EXECUTOR.asCoroutineDispatcher()
替代方案
其实还有一些其他的方式可以让我们使用协程来实现这一行为。不过,这些解决方案不是在任何条件下都能有条理地实现。下面就让我们看看一些替代方案,以及为何适用或者不适用,何时使用或者不使用它们。
? GlobalScope
下面是几个不应该使用 GlobalScope 的理由:
- 诱导我们写出硬编码值 。直接使用 GlobalScope 可能会让我们倾向于写出硬编码的调度器,这是一种很差的实践方式。
- 导致测试非常困难 。由于您的代码会在一个不受控制的作用域中执行,您将无法对从中启动的任务进行管理。
- 就如同我们对 applicationScope 所做的那样,您无法为所有协程都提供一个通用的、内建于作用域中的 CoroutineContext。相反,您必须传递一个通用的 CoroutineContext 给 GlobalScope 启动的所有协程。
建议: 不要直接使用它。
? Android 中的 ProcessLifecycleOwner 作用域
在 Android 中的 androidx.lifecycle:lifecycle-process 库中,有一个 applicationScope,您可以使用 ProcessLifecycleOwner.get().lifecycleScope 来调用它。
在使用它时,您需要注入一个 LifecycleOwner 来代替我们之前注入的 CoroutineScope。在生产环境中,您需要传入 ProcessLifecycleOwner.get();而在单元测试中,您可以用 LifecycleRegistry 来创建一个虚拟的 LifecycleOwner。 注意,这个作用域的默认 CoroutineContext 是 Dispatchers.Main.immediate,所以它可能不太适合去执行后台任务。就像使用 GlobalScope 时那样,您也需要传递一个通用的 CoroutineContext 到所有通过 GlobalScope 启动的协程中。
由于上述原因,此替代方案相比起直接在 Application 类中创建一个 CoroutineScope 要麻烦许多。而且,我个人不喜欢在 ViewModel 或 Presenter 层之下与 Android lifecycle 建立关系,我希望这些层级是平台无关的。
建议: 不要直接使用它。
?? 特别说明**
如果您将您的 applicationScope 中的 CoroutineContext 等于 GlobalScope 或 ProcessLifecycleOwner.get().lifecycleScope,您就可以像下面这样直接使用它:
class MyApplication : Application() {
val applicationScope = GlobalScope
}
您仍然可以获得上文所述的所有优点,并且将来可以根据需要轻松进行更改。
? ? 使用 NonCancellable
正如您在本系列第二篇文章 协程中的取消和异常 | 取消操作详解 中看到的,您可以使用 withContext(NonCancellable) 在被取消的协程中调用挂起函数。我们建议您使用它来进行可挂起的代码清理,但是,您不应该滥用它。
这样做的风险很高,因为您将会无法控制协程的执行。确实,它可以使代码更简洁,可读性更强,但与此同时,它也可能在将来引起一些无法预测的问题。
使用示例如下:
class Repository(
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
withContext(NonCancellable){
veryImportantOperation()
}
}
}
}
尽管这个方案很有诱惑力,但是您可能无法总是知道 someImportantOperation() 背后有什么逻辑。它可能是一个扩展库;也可能是一个接口背后的实现。它可能会导致各种各样的问题:
- 您将无法在测试中结束这些操作;
- 使用延迟的无限循环将永远无法被取消;
- 从其中收集 Flow 会导致 Flow 也变得无法从外部取消;
- …...
而这些问题会导致出现细微且非常难以调试的错误。
建议: 仅用它来挂起清理操作相关的代码。
每当您需要执行一些超出当前作用域范围的工作时,我们都建议您在您自己的 Application 类中创建一个自定义作用域,并在此作用域中执行协程。同时要注意,在执行这类任务时,避免使用 GlobalScope、ProcessLifecycleOwner 作用域或 NonCancellable。
相关推荐
- ubuntu单机安装open-falcon极度详细操作
-
备注:以下操作均由本人实际操作并得到验证,喜欢的同学可尝试操作安装。步骤一1.1环境准备(使用系统:ubuntu18.04)1.1.1安装redisubuntu下安装(参考借鉴:https://...
- Linux搭建promtail、loki、grafana轻量日志监控系统
-
一:简介日志监控告警系统,较为主流的是ELK(Elasticsearch、Logstash和Kibana核心套件构成),虽然优点是功能丰富,允许复杂的操作。但是,这些方案往往规模复杂,资源占用高,...
- 一文搞懂,WAF阻止恶意攻击的8种方法
-
WAF(Web应用程序防火墙)是应用程序和互联网流量之间的第一道防线,它监视和过滤Internet流量以阻止不良流量和恶意请求,WAF是确保Web服务的可用性和完整性的重要安全解决方案。它...
- 14配置appvolume(ios14.6配置文件)
-
使用AppVolumes应用程序功能,您可以管理应用程序的整个生命周期,包括打包、更新和停用应用程序。您还可以自定义应用程序分配,以向最终用户提供应用程序的特定版本14.1安装appvolume...
- 目前流行的缺陷管理工具(缺陷管理方式存在的优缺点)
-
摘自:https://blog.csdn.net/jasonteststudy/article/details/7090127?utm_medium=distribute.pc_relevant.no...
- 开源数字货币交易所开发学习笔记(2)——SpringCloud
-
前言码云(Gitee)上开源数字货币交易所源码CoinExchange的整体架构用了SpringCloud,对于经验丰富的Java程序员来说,可能很简单,但是对于我这种入门级程序员,还是有学习的必要的...
- 开发JAX-RPC Web Services for WebSphere(下)
-
在开发JAX-RPCWebServicesforWebSphere(上)一文中,小编为大家介绍了如何创建一个Web服务项目、如何创建一个服务类和Web服务,以及部署项目等内容。接下来小编将为大...
- CXF学习笔记1(cxf client)
-
webservice是发布服务的简单并实用的一种技术了,个人学习了CXF这个框架,也比较简单,发布了一些笔记,希望对笔友收藏并有些作用哦1.什么是webServicewebService让一个程序可...
- 分布式RPC最全详解(图文全面总结)
-
分布式通信RPC是非常重要的分布式系统组件,大厂经常考察的Dubbo等RPC框架,下面我就全面来详解分布式通信RPC@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题合集》...
- Oracle WebLogic远程命令执行0day漏洞(CVE-2019-2725补丁绕过)预警
-
概述近日,奇安信天眼与安服团队通过数据监控发现,野外出现OracleWebLogic远程命令执行漏洞最新利用代码,此攻击利用绕过了厂商今年4月底所发布的最新安全补丁(CVE-2019-2725)。由...
- Spring IoC Container 原理解析(spring中ioc三种实现原理)
-
IoC、DI基础概念关于IoC和DI大家都不陌生,我们直接上martinfowler的原文,里面已经有DI的例子和spring的使用示例《InversionofControlContainer...
- Arthas线上服务器问题排查(arthas部署)
-
1Arthas(阿尔萨斯)能为你做什么?这个类从哪个jar包加载的?为什么会报各种类相关的Exception?我改的代码为什么没有执行到?难道是我没commit?分支搞错了?遇到问题无法在...
- 工具篇之IDEA功能插件HTTP_CLENT(idea2021插件)
-
工具描述:Java开发人员通用的开发者工具IDEA集成了HTTPClient功能,之后可以无需单独安装使用PostMan用来模拟http请求。创建方式:1)简易模式Tools->HTTPCl...
- RPC、Web Service等几种远程监控通信方式对比
-
几种远程监控通信方式的介绍一.RPCRPC使用C/S方式,采用http协议,发送请求到服务器,等待服务器返回结果。这个请求包括一个参数集和一个文本集,通常形成“classname.meth...
- 《github精选系列》——SpringBoot 全家桶
-
1简单总结1SpringBoot全家桶简介2项目简介3子项目列表4环境5运行6后续计划7问题反馈gitee地址:https://gitee.com/yidao620/springbo...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- maven镜像 (69)
- undefined reference to (60)
- zip格式 (63)
- oracle over (62)
- date_format函数用法 (67)
- 在线代理服务器 (60)
- shell 字符串比较 (74)
- x509证书 (61)
- localhost (65)
- java.awt.headless (66)
- syn_sent (64)
- settings.xml (59)
- 弹出窗口 (56)
- applicationcontextaware (72)
- my.cnf (73)
- httpsession (62)
- pkcs7 (62)
- session cookie (63)
- java 生成uuid (58)
- could not initialize class (58)
- beanpropertyrowmapper (58)
- word空格下划线不显示 (73)
- jar文件 (60)
- jsp内置对象 (58)
- makefile编写规则 (58)