個人覺得這個在使用協程過程中是個很好的說明,一般根據直覺的話,很有可能寫出某些反模式的用法。
介紹
依我之見,我決定寫幾點,來表明在使用協程的過程中,應當或者不應當的幾件事(或者至少盡力避免)。
用 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
}
}