Android 使协同程序的一部分在取消后继续

Android 使协同程序的一部分在取消后继续,android,kotlin,kotlin-coroutines,Android,Kotlin,Kotlin Coroutines,我有一个文件管理类,可以保存一个大文件。文件管理器类是一个应用程序单例,因此它比我的UI类更有效。我的活动/片段可以从协同程序调用文件管理器的savesuspend函数,然后在UI中显示成功或失败。例如: //In MyActivity: private fun saveTheFile() = lifecycleScope.launch { try { myFileManager.saveBigFile() myTextView.text = "Succe

我有一个文件管理类,可以保存一个大文件。文件管理器类是一个应用程序单例,因此它比我的UI类更有效。我的活动/片段可以从协同程序调用文件管理器的
save
suspend函数,然后在UI中显示成功或失败。例如:

//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
        //...
    }
}
这种方法的问题是,如果活动完成,我不希望保存操作中止。如果活动在
withContext
块开始运行之前被销毁,或者
withContext
块中有任何挂起点,则将无法完成保存,因为协同程序将被取消

我想做的是,文件总是保存的。如果活动仍然存在,那么我们可以在完成时显示UI更新

我认为这样做的一个方法可能是像这样从suspend函数启动一个新的
coroutineScope
,但是当其父作业被取消时,这个作用域似乎仍然被取消

suspend fun saveBigFile() = coroutineScope {
    //...
}
//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
        //...
    }
}

我认为另一种选择可能是让它成为一个常规函数,在完成时更新一些LiveData。活动可以观察实时数据以获得结果,并且由于LiveData在生命周期观察者被销毁时自动删除它们,因此活动不会泄漏给FileManager。我想避免这种模式,如果像上面这样不太复杂的事情可以做的话

//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
}
//在MyActivity中:
私人娱乐存储文件(){
val result=myFileManager.saveBigFile()
结果观察(this@MyActivity) {
myTextView.text=何时(它){
true->“已成功保存文件”
else->“未能保存文件”
}
}
}
//在MyFileManager中
fun saveBigFile():LiveData{
//设置参数
//...
val liveData=MutableLiveData()
MainScope().launch{
val success=withContext(Dispatchers.IO){
//保存文件
//...
}
liveData.value=成功
}
返回实时数据
}

您可以使用
不可取消
包装不希望取消的位

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

如果您的代码的生命周期限定为整个应用程序的生命周期,那么这就是
GlobalScope
的一个用例。但是,仅仅说
GlobalScope.launch
不是一个好策略,因为您可能会启动几个可能存在冲突的并发文件操作(这取决于您的应用程序的详细信息)。推荐的方法是使用一个全局范围的
参与者
,作为执行者服务

基本上,你可以说

@ObsoleteCoroutinesApi
val executor = GlobalScope.actor<() -> Unit>(Dispatchers.IO) {
    for (task in channel) {
        task()
    }
}
请注意,这仍然不是一个很好的解决方案,它将
myTextView
保留到其生命周期之外。不过,将UI通知与视图分离是另一个主题


actor
被标记为“过时的协同程序API”,但这只是一个预先通知,它将在Kotlin的未来版本中被一个更强大的替代品所取代。这并不意味着它坏了或不受支持。

我试过这个,它似乎实现了我描述的我想要的。FileManager类有自己的作用域,但我认为它也可以是GlobalScope,因为它是一个单例类

我们从协同程序在其自身范围内启动一个新作业。这是从一个单独的函数中完成的,以消除关于作业范围的任何模糊性。我使用
async
对于另一个任务,我可以冒泡出UI应该响应的异常

然后在启动之后,我们等待异步作业回到原始范围<代码>等待()暂停,直到作业完成并传递任何抛出(在我的情况下,我希望IOExceptions弹出,以便UI显示错误消息)。因此,如果原始作用域被取消,它的协程永远不会等待结果,但启动的作业会一直滚动,直到它正常完成。我们希望确保始终处理的任何异常都应该在异步函数中处理。否则,如果原来的工作被取消,他们就不会冒泡

suspend fun saveBigFile() = coroutineScope {
    //...
}
//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
        //...
    }
}


也许你应该在服务中完成这样的工作,即使你的活动/应用程序被杀死,这些工作也需要完成。因为协同程序就像线程一样,其生命周期取决于它运行的进程。服务使您保证您的工作将被完成,并且不会受到活动/应用程序终止的影响。服务也会在您的应用程序中运行,因此不保证不会被终止。如果你把它作为前台服务,它就不太可能被杀死。但我从未见过任何应用程序启动前台服务只是为了保存文件。很可能,文件操作不会花费足够长的时间,应用程序可能会被破坏以节省RAM。另一种方法可能是使用workmanager,它不会受到应用程序终止或重新启动的影响。从文档中看,这似乎应该行得通。但我开始感觉到,有任何协同程序不能很好地处理取消,这是一种很大的代码味道。NonCancellable的文档表明,它是您应该很少需要的东西,并且只在
finally
块中使用。协同程序忽略取消请求似乎是一种意外行为。也许LiveData是处理这一问题的最佳方式。Kotlinconf提供的取消视频效果很好,包括不可取消如果你需要做一些原子性的事情,不能中途取消,那么你就属于少数情况之一。暂时阻止取消是可以的,只需确保您的协同路由的作用域已确定。我们是否可以通过使用为文件管理器的生命周期创建的协同路由作用域(这是为应用程序的生命周期创建的单例)获得与您描述的相同的结果?关于你的例子,如果我不在乎泄露活动的视图,我就不必担心取消——我会使用lifecycleScope以外的东西来启动此作业