kotin 協程中的模式與反模式

原文鏈接

個人覺得這個在使用協程過程中是個很好的說明,一般根據直覺的話,很有可能寫出某些反模式的用法。

介紹

依我之見,我決定寫幾點,來表明在使用協程的過程中,應當或者不應當的幾件事(或者至少盡力避免)。

用 coroutineScope 或者SupervisorJob 來封裝async調用來處理異常

❌ async代碼塊可能會拋異常,別指望用try/catch可以包裹處理

val job: Job = Job()
val scope = CoroutineScope(Dispatchers.Default + job)
// may throw Exception
fun doWork(): Deferred<String> = scope.async { ... }   // (1)
fun loadData() = scope.launch {
    try {
        doWork().await()                               // (2)
    } catch (e: Exception) { ... }
}

上面的例子中 doWork 函數啓動了一個新的協程,它可能會拋出一個爲處理的異常。如果你用try/catch代碼塊來包裹它的話,你會發現依然會崩,爲啥會這樣,是因爲任何job 的子孩子崩潰立刻導致了其父類的失敗。
✅ 使用SupervisorJob 是避免此種崩潰的一種方法

子任務的崩潰和取消不會導致 supervisor job 的崩潰,也不影響它的其他子任務

val job = SupervisorJob()                               // (1)
val scope = CoroutineScope(Dispatchers.Default + job)

// may throw Exception
fun doWork(): Deferred<String> = scope.async { ... }

fun loadData() = scope.launch {
    try {
        doWork().await()
    } catch (e: Exception) { ... }
}

注意:上面的代碼僅在你顯示調用SupervisorJob來運行async代碼塊的時候生效。像下面這種在父協程的scope 中啓動async還是會搞崩你的程序。

val job = SupervisorJob()                               
val scope = CoroutineScope(Dispatchers.Default + job)

fun loadData() = scope.launch {
    try {
        async {                                         // (1)
            // may throw Exception 
        }.await()
    } catch (e: Exception) { ... }
}

✅ 另一種可能避免這種崩潰的做法,或許是更好的做法,是用coroutineScope 來包裹你的async代碼塊。這樣的話,如果async代碼塊有異常,所有coroutineScope 中的創建的協程也會被取消掉,並且也不會波及影響其他scope

val job = SupervisorJob()                               
val scope = CoroutineScope(Dispatchers.Default + job)

// may throw Exception
suspend fun doWork(): String = coroutineScope {     // (1)
    async { ... }.await()
}

fun loadData() = scope.launch {                       // (2)
    try {
        doWork()
    } catch (e: Exception) { ... }
}

或者,你還可以在async代碼塊中處理異常。

優先爲根協程配置 Dispatchers.Main的調度器

❌如果你需要執行一個後臺任務並在根協程中更新UI的話,別用非Dispatchers.Main的其他調度器。

val scope = CoroutineScope(Dispatchers.Default)          // (1)

fun login() = scope.launch {
    withContext(Dispatcher.Main) { view.showLoading() }  // (2)  
    networkClient.login(...)
    withContext(Dispatcher.Main) { view.hideLoading() }  // (2)
}

上面這個例子在根協程中用了一個Dispatchers.Default的調度器,這樣做就會導致每次我們想更新UI,還得用withContext切換調度器。

✅大多數情況下,使用Dispatchers.Main調度器代碼會更簡潔,並可避免顯示的上下文調度器切換。

val scope = CoroutineScope(Dispatchers.Main)

fun login() = scope.launch {
    view.showLoading()    
    withContext(Dispatcher.IO) { networkClient.login(...) }
    view.hideLoading()
}

避免濫用 async /await

❌ 如果你使用async函數並且馬上又await的話,我建議你別那樣搞了。

launch {
    val data = async(Dispatchers.Default) { /* code */ }.await()
}

✅ 如果你想切換協程上下文又想立刻掛起父協程,那麼使用withContext 是個更好的做法。

launch {
    val data = withContext(Dispatchers.Default) { /* code */ }
}

性能上的考慮先放一邊(儘管async創建了新的協程來處理任務),從語義上來說async意味着你在後臺創建了幾個協程,而又只是爲了等待他們完成任務。

不要取消 scope 級別的任務

❌ 如果你需要取消協程,首先不要取消scope 級別的任務

class WorkManager {
    val job = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + job)
    
    fun doWork1() {
        scope.launch { /* do work */ }
    }
    
    fun doWork2() {
        scope.launch { /* do work */ }
    }
    
    fun cancelAllWork() {
        job.cancel()
    }
}

fun main() {
    val workManager = WorkManager()
    
    workManager.doWork1()
    workManager.doWork2()
    workManager.cancelAllWork()
    workManager.doWork1() // (1)
}

上面代碼的問題是,如果你取消scope級別的任務時,實際上是將其置爲一種完成的狀態。一個完成狀態下的任務,協程將不會再執行。

✅當你想要取消指定scope中的所有協程時,可以用cancelChildren函數。這也是取消獨立子任務的好的方式。

class WorkManager {
    val job = SupervisorJob()
    val scope = CoroutineScope(Dispatchers.Default + job)
    
    fun doWork1(): Job = scope.launch { /* do work */ } // (2)
    
    fun doWork2(): Job = scope.launch { /* do work */ } // (2)
    
    fun cancelAllWork() {
        scope.coroutineContext.cancelChildren()         // (1)                             
    }
}
fun main() {
    val workManager = WorkManager()
    
    workManager.doWork1()
    workManager.doWork2()
    workManager.cancelAllWork()
    workManager.doWork1()
}

寫suspend 函數時別用隱式的調度器

❌寫suspend 函數時別用一個隱式的協程調度器,而最終這個函數又有可能在其他某個調度器環境執行。

suspend fun login(): Result {
    view.showLoading()
    
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    view.hideLoading()
    
    return result
}

上面這個例子中 ,login函數是一個 suspend函數,如果你用一個非Dispatcher.Main的調度器來執行,很可能導致崩潰。

道理也很簡單,因爲view的操作是要主線程,也就是UI線程的,Dispatcher.Main 之外的,都不好使。

launch(Dispatcher.Main) {     // (1) no crash
    val loginResult = login()
    ...
}

launch(Dispatcher.Default) {  // (2) cause crash
    val loginResult = login()
    ...
}

下面的異常,各位安卓老鐵們是不是常遇到,就是不要在非UI線程更新UI。

CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

✅下面是可以讓你的login 函數在任何其他調度器運行的寫法。就是給你的函數指定Dispatcher.Main調度器,在其他任何別的調度器上下文運行的時候,都會切到UI線程,再也不擔心崩潰了。

suspend fun login(): Result = withContext(Dispatcher.Main) {
    view.showLoading()
    
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    
    view.hideLoading()
	return result
}

現在我們可以從任何其他調度器上下文運行我們的login函數了。

launch(Dispatcher.Main) {     // (1) no crash
    val loginResult = login()
    ...
}

launch(Dispatcher.Default) {  // (2) no crash ether
    val loginResult = login()
    ...
}

別用global scope

❌ 在你的應用中,別用到處用 GlobalScope

GlobalScope.launch {
    // code
}

GlobalScope是用來啓動最頂層的協程的,存活於整個應用程序的生命週期,而且不能取消 寫到這裏,我彷彿看到了老鐵們放亮的招子,什麼,整個應用程序生命週期,那我是不是可以。。。

這個scope範圍內的代碼通常是用於應用程序定義其應用範圍的 CoroutineScope
使用GlobalScope 來啓動async和launch 是極度被摒棄的。

✅在安卓中,最好是Activity, Fragment ,View 或者ViewModel 的生命週期級別的scope 來啓用協程。

class MainActivity : AppCompatActivity(), CoroutineScope {
    
    private val job = SupervisorJob()
    
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job
    
    override fun onDestroy() {
        super.onDestroy()
        coroutineContext.cancelChildren()
    }
    
    fun loadData() = launch {
        // code
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章