Make part of coroutine continue past cancellation
我有一个可以保存大文件的文件管理类。文件管理器类是一个应用程序单例,因此它比我的 UI 类寿命更长。我的 Activity/Fragment 可以从协程调用文件管理器的 save 挂起函数,然后在 UI 中显示成功或失败。例如:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch { try { myFileManager.saveBigFile() myTextView.text =”Successfully saved file” } catch (e: IOException) { myTextView.text =”Failed to save file” } } //In MyFileManager withContext(Dispatchers.IO) { |
这种方法的问题是,如果 Activity 完成,我不希望保存操作被中止。如果活动在 withContext 块开始之前被销毁,或者如果 withContext 块中有任何暂停点,则保存将不会完成,因为协程将被取消。
我想要发生的是文件总是被保存。如果 Activity 仍然存在,那么我们可以在完成时显示 UI 更新。
我认为一种方法可能是像这样从挂起函数启动一个新的 coroutineScope,但是当它的父作业被取消时,这个范围似乎仍然被取消。
1
2 3 |
suspend fun saveBigFile() = coroutineScope {
//… } |
我认为另一种选择可能是让这个函数成为一个常规函数,在完成时更新一些 LiveData。 Activity 可以观察结果的实时数据,并且由于 LiveData 在生命周期观察者被销毁时会自动删除它们,因此 Activity 不会泄漏到 FileManager。如果可以改用上述不那么复杂的方法,我想避免这种模式。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//In MyActivity:
private fun saveTheFile() { val result = myFileManager.saveBigFile() result.observe(this@MyActivity) { myTextView.text = when (it) { true ->”Successfully saved file” else ->”Failed to save file” } } } //In MyFileManager |
- 可能您应该在服务中进行此类工作,即使您的活动/应用程序被杀死也需要完成。因为协程就像线程一样,其生命周期取决于它运行的进程。服务使您保证您的工作将完成并且不会受到活动/应用程序被终止的影响。
- 服务也在您的应用程序中运行,因此不能保证它不会被杀死。如果你把它设为前台服务,它就不太可能被杀死。但我从未见过任何应用程序启动前台服务只是为了保存文件。很可能,文件操作不会花费足够长的时间来承受应用程序被拆除以节省 RAM 的风险。
- 另一种方法可能是使用不会受到应用程序终止或重启影响的 workmanager。
你可以用 NonCancellable 包裹你不想被取消的位。
1
2 3 4 5 |
// May cancel here.
withContext(Dispatchers.IO + NonCancellable) { // Will complete, even if cancelled. } // May cancel here. |
- 从文档看来,这应该可行。但是我开始感觉到,如果协程不能很好地取消,那是一种很大的代码气味。 NonCancellable 的文档暗示您应该很少需要它,并且只能在 finally 块中。协程忽略取消请求似乎是一种意外行为。也许 LiveData 是处理这个问题的最佳方式。
- Kotlinconf 取消的好视频,包括 NonCancellable youtu.be/w0kfnydnFWI?t=1160
- 如果您需要做一些原子性的事情,并且不能在中途取消,那么您就处于罕见的情况之一。暂时防止取消是可以的,只要确保你的协程是作用域的。
如果您的代码的生命周期限定为整个应用程序的生命周期,那么这是 GlobalScope 的一个用例。但是,仅仅说 GlobalScope.launch 并不是一个好的策略,因为您可能会启动多个可能发生冲突的并发文件操作(这取决于您的应用程序的详细信息)。推荐的方法是使用全局范围的 actor,作为执行器服务的角色。
基本上可以说
1
2 3 4 5 6 |
@ObsoleteCoroutinesApi
val executor = GlobalScope.actor<() -> Unit>(Dispatchers.IO) { for (task in channel) { task() } } |
并像这样使用它:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 |
private fun saveTheFile() = lifecycleScope.launch {
executor.send { try { myFileManager.saveBigFile() withContext(Main) { myTextView.text =”Successfully saved file” } } catch (e: IOException) { withContext(Main) { myTextView.text =”Failed to save file” } } } } |
请注意,这仍然不是一个很好的解决方案,它会在其生命周期之后保留 myTextView。不过,将 UI 通知与视图分离是另一个主题。
actor 被标记为”过时的协程 API”,但这只是一个预告,它将在未来的 Kotlin 版本中被更强大的替代方案所取代。这并不意味着它已损坏或不受支持。
- 通过使用为文件管理器的生命周期创建的 CoroutineScope(它是一个为应用程序生命而存在的单例),我们会得到与您描述的相同的结果吗?关于你的例子,如果我不关心泄露 Activity 的视图,我首先不需要担心取消——我会使用生命周期范围以外的东西来启动这项工作。
- 至于第一点,你总是可以写GlobalScope.launch(Dispatchers.IO),但这仍然会留下泄漏问题。您必须从提交的后台作业中删除对视图的所有引用,这通常意味着使用 MVVM,以便您可以引用 ViewModel。
- @MarkoTopoInk,您有推荐的方法来处理 @ObsoleteCoroutinesApi 注释吗?我不想在我的代码中传播这个注释,而是将它限制在我直接使用演员的地方,所以有一天我可以在演员被弃用时修改 API。这样做的唯一方法是根本不使用注释并保持警告不变,还是有办法仅在此类或函数上抑制它?
我试过这个,它似乎可以按照我描述的那样做。 FileManager 类有自己的范围,但我想它也可以是 GlobalScope,因为它是一个单例类。
我们从协程在其自身范围内启动一项新工作。这是通过一个单独的函数完成的,以消除关于工作范围的任何歧义。我使用 async
为这个其他工作,所以我可以冒泡 UI 应该响应的异常。
然后在启动后,我们等待异步作业返回原始范围。 await() 挂起,直到作业完成并传递任何抛出(在我的情况下,我希望 IOExceptions 冒泡以便 UI 显示错误消息)。因此,如果原始范围被取消,它的协程永远不会等待结果,但启动的作业会继续滚动,直到它正常完成。我们要确保始终处理的任何异常都应在 async 函数中处理。否则,如果取消原??始作业,它们将不会冒泡。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch { try { myFileManager.saveBigFile() myTextView.text =”Successfully saved file” } catch (e: IOException) { myTextView.text =”Failed to save file” } } class MyFileManager private constructor(app: Application): suspend fun saveBigFile() { val deferred = saveBigFileAsync() private fun saveBigFileAsync() = async(Dispatchers.IO) { |
来源:https://www.codenong.com/60155439/