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
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值