[英]Make part of coroutine continue past cancellation
我有一個可以保存大文件的文件管理類。 文件管理器類是一個應用程序單例,因此它的壽命比我的 UI 類長。 我的 Activity/Fragment 可以從協程調用文件管理器的save
掛起功能,然后在 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
//...
}
}
這種方法的問題在於,如果 Activity 完成,我不希望保存操作被中止。 如果活動在withContext
塊開始之前被銷毀,或者如果withContext
塊中有任何暫停點,那么保存將不會完成,因為協程將被取消。
我想要發生的是文件總是被保存。 如果 Activity 仍然存在,那么我們可以在完成時顯示 UI 更新。
我認為一種方法可能是像這樣從掛起函數啟動一個新的coroutineScope
,但是當它的父作業被取消時,這個范圍似乎仍然被取消。
suspend fun saveBigFile() = coroutineScope {
//...
}
我認為另一種選擇可能是使其成為一個常規函數,在它完成時更新一些 LiveData。 Activity 可以觀察結果的實時數據,並且由於 LiveData 在生命周期觀察者被銷毀時會自動刪除它們,因此 Activity 不會泄漏到 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
}
您可以使用NonCancellable
包裝不想取消的NonCancellable
。
// May cancel here.
withContext(Dispatchers.IO + NonCancellable) {
// Will complete, even if cancelled.
}
// May cancel here.
如果您的代碼的生命周期范圍為整個應用程序的生命周期,那么這是GlobalScope
一個用例。 但是,僅僅說GlobalScope.launch
並不是一個好的策略,因為您可能會啟動多個可能發生沖突的並發文件操作(這取決於您的應用程序的詳細信息)。 推薦的方法是使用全局作用域的actor
,充當執行器服務的角色。
基本上,你可以說
@ObsoleteCoroutinesApi
val executor = GlobalScope.actor<() -> Unit>(Dispatchers.IO) {
for (task in channel) {
task()
}
}
並像這樣使用它:
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
在其生命周期之后保留myTextView
。 不過,將 UI 通知與視圖分離是另一個主題。
actor
被標記為“過時的協程 API”,但這只是一個預先通知,它將在 Kotlin 的未來版本中被更強大的替代品所取代。 這並不意味着它已損壞或不受支持。
我試過這個,它似乎做了我所描述的我想要的。 FileManager 類有自己的作用域,不過我想它也可以是 GlobalScope,因為它是一個單例類。
我們從協程在其自己的范圍內啟動一個新作業。 這是從一個單獨的函數完成的,以消除有關工作范圍的任何歧義。 我將async
用於其他工作,以便我可以冒出 UI 應該響應的異常。
然后在啟動后,我們等待異步作業回到原始范圍。 await()
掛起直到作業完成並傳遞任何拋出(在我的情況下,我希望 IOExceptions 冒泡以便 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"
}
}
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
//...
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.