在本系列第二篇文章 協程中的取消和異常 | 取消操做詳解 中,咱們學到,當一個任務再也不被須要時,正確地退出十分的重要。在 Android 中,您可使用 Jetpack 提供的兩個 CoroutineScopes: viewModelScope.viewModelScope:kotlinx.coroutines.CoroutineScope) 和 lifecycleScope,它們能夠在 Activity、Fragment、Lifecycle 完成時退出正在運行的任務。若是您正在建立本身的 CoroutineScope,記得將它綁定到某個任務中,並在須要的時候取消它。html
然而,在有些狀況下,您會但願即便用戶離開了當前界面,操做依然可以執行完成。所以,您就不會但願任務被取消,例如,向數據庫寫入數據或者向您的服務器發送特定類型的請求。java
下面咱們就來介紹實現此類狀況的模式。android
協程會在您的應用進程活動期間執行。若是您須要執行一個可以在應用進程以外活躍的操做 (好比向遠程服務器發送日誌),在 Android 平臺上建議使用 WorkManager。WorkManager 是一個擴展庫,用於那些預期會在未來的某個時間點執行的重要操做。ios
請針對那些在當前進程中有效的操做使用協程,同時保證能夠在用戶關閉應用時取消操做 (例如,進行一個您但願緩存的網絡請求)。那麼,實現這類操做的最佳實踐是什麼呢?git
因爲本文所介紹的模式是在協程的其它最佳實踐的基礎之上實現的,咱們能夠藉此機會回顧一下:github
不要在建立協程或調用 withContext 時硬編碼調度器。數據庫
✅ 好處: 便於測試。您能夠在進行單元測試或儀器測試時輕鬆替換掉它們。緩存
若是是僅與 UI 相關的操做,則能夠在 UI 層執行。若是您認爲這條最佳實踐在您的工程中不可行,則頗有多是您沒有遵循第一條最佳實踐 (測試沒有注入調度器的 ViewModel 會變得更加困難;這種狀況下,暴露出掛起函數會使測試變得可行)。服務器
✅ 好處: UI 層應該儘可能簡潔,而且不直接觸發任何業務邏輯。做爲代替,應當將響應能力轉移到 ViewModel 或 Presenter 層實現。在 Android 中,測試 UI 層須要執行插樁測試,而執行插樁測試須要運行一個模擬器。網絡
若是您須要建立協程,請使用 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 啓動新的協程:
下面是使用 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() } } } }
可是,此方法有下面幾個注意事項,使用的時候須要注意:
因爲咱們可能須要同時注入調度器和 CoroutineScop,那麼這些場景裏分別須要注入什麼呢?
測試時要注入什麼
🔖 說明文檔:
其實還有一些其餘的方式可讓咱們使用協程來實現這一行爲。不過,這些解決方案不是在任何條件下都能有條理地實現。下面就讓咱們看看一些替代方案,以及爲什麼適用或者不適用,什麼時候使用或者不使用它們。
下面是幾個不該該使用 GlobalScope 的理由:
建議: 不要直接使用它。
在 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 }
您仍然能夠得到上文所述的全部優勢,而且未來能夠根據須要輕鬆進行更改。
正如您在本系列第二篇文章 協程中的取消和異常 | 取消操做詳解 中看到的,您可使用 withContext(NonCancellable) 在被取消的協程中調用掛起函數。咱們建議您使用它來進行可掛起的代碼清理,可是,您不該該濫用它。
這樣作的風險很高,由於您將會沒法控制協程的執行。確實,它可使代碼更簡潔,可讀性更強,但與此同時,它也可能在未來引發一些沒法預測的問題。
使用示例以下:
class Repository( private val ioDispatcher: CoroutineDispatcher ) { suspend fun doWork() { withContext(ioDispatcher) { doSomeOtherWork() withContext(NonCancellable){ veryImportantOperation() } } } }
儘管這個方案頗有誘惑力,可是您可能沒法老是知道 someImportantOperation() 背後有什麼邏輯。它多是一個擴展庫;也多是一個接口背後的實現。它可能會致使各類各樣的問題:
而這些問題會致使出現細微且很是難以調試的錯誤。
建議: 僅用它來掛起清理操做相關的代碼。
每當您須要執行一些超出當前做用域範圍的工做時,咱們都建議您在您本身的 Application 類中建立一個自定義做用域,並在此做用域中執行協程。同時要注意,在執行這類任務時,避免使用 GlobalScope、ProcessLifecycleOwner 做用域或 NonCancellable。