原文链接
个人觉得这个在使用协程过程中是个很好的说明,一般根据直觉的话,很有可能写出某些反模式的用法。
依我之见,我决定写几点,来表明在使用协程的过程中,应当或者不应当的几件事(或者至少尽力避免)。
❌ 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代码块中处理异常。
❌如果你需要执行一个后台任务并在根协程中更新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的话,我建议你别那样搞了。
launch {
val data = async(Dispatchers.Default) { /* code */ }.await()
}
✅ 如果你想切换协程上下文又想立刻挂起父协程,那么使用withContext 是个更好的做法。
launch {
val data = withContext(Dispatchers.Default) { /* code */ }
}
性能上的考虑先放一边(尽管async创建了新的协程来处理任务),从语义上来说async意味着你在后台创建了几个协程,而又只是为了等待他们完成任务。
❌ 如果你需要取消协程,首先不要取消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 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()
...
}
❌ 在你的应用中,别用到处用 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
}
}