关于android:Make part of coroutine continue past cancel

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
suspend fun saveBigFile() {
    //Set up the parameters
    //...

    withContext(Dispatchers.IO) {
        //Save the file
        //...
    }
}

这种方法的问题是,如果 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
fun saveBigFile(): LiveData<Boolean> {
    //Set up the parameters
    //...
    val liveData = MutableLiveData<Boolean>()
    MainScope().launch {
        val success = withContext(Dispatchers.IO) {
            //Save the file
            //...
        }
        liveData.value = success
    }
    return liveData
}


你可以用 NonCancellable 包裹你不想被取消的位。

1
2
3
4
5
// May cancel here.
withContext(Dispatchers.IO + NonCancellable) {
    // Will complete, even if cancelled.
}
// May cancel here.


如果您的代码的生命周期限定为整个应用程序的生命周期,那么这是 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 版本中被更强大的替代方案所取代。这并不意味着它已损坏或不受支持。


我试过这个,它似乎可以按照我描述的那样做。 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):
    CoroutineScope by MainScope() {

    suspend fun saveBigFile() {
        //Set up the parameters
        //...

        val deferred = saveBigFileAsync()
        deferred.await()
    }

    private fun saveBigFileAsync() = async(Dispatchers.IO) {
        //Save the file
        //...
    }
}