使用 Kotlin 协程时应该注意的事情

84 篇文章 5 订阅

使用 coroutineScope 包装异步调用或使用 SupervisorJob 处理异常

❌ 如果异步块可能抛出异常,请不要依赖于用 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 函数启动新的协程 (1),这可能会引发未处理的异常。如果您尝试使用 try/catch 块 (2) 包装 doWork,它仍然会崩溃。

发生这种情况是因为作业的任何子作业的失败都会导致其父作业立即失败。

✅ 避免崩溃的一种方法是使用 SupervisorJob(1)。

子级的失败或取消不会导致主管作业失败,也不会影响其其他子级。

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 在协程范围内显式运行异步时,这才有效。因此,下面的代码仍然会使您的应用程序崩溃,因为异步是在父协程 (1) 的范围内启动的。

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

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

✅ 另一种避免崩溃的方法(这是更好的方法)是使用 coroutineScope

(1)包装 async。现在,当 async 内部发生异常时,它将取消在此范围内创建的所有其他协程,而不会触及外部范围。 (2)可以在异步块内处理异常。

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) { ... }
}

首选根协程的主调度程序

❌ 如果需要在根协程内执行后台工作并更新 UI,请不要使用非主调度程序启动它。

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

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

在上面的示例中,使用默认调度程序 (1) 的作用域启动根协程。通过这种方法,每次用户触摸界面时,都必须切换上下文(2) ✅ 在大多数情况下,最好使用主调度程序创建作用域,这会导致更简单的代码和不太明确的上下文切换。

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 */ }
}

从性能角度来看,这并不是一个大问题(即使异步会创建新的协程来完成工作),但从语义上讲,异步意味着您想要在后台启动多个协程,然后才等待它们。

避免取消范围作业

❌ 如果你需要取消协程,首先不要取消作用域作业。

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)
}

上述代码的问题在于,当我们取消作业时,我们将其置于已完成状态。在已完成作业范围内启动的协程将不会被执行 (1)。 ✅ 当你想取消特定作用域的所有协程时,可以使用cancelChildren函数。此外,提供取消个别作业的可能性也是一个很好的做法 (2)。

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 fun login(): Result {
    view.showLoading()
    
    val result = withContext(Dispatcher.IO) {  
        someBlockingCall() 
    }
    view.hideLoading()
    
    return result
}

在上面的示例中,登录函数是一个挂起函数,如果从使用非主调度程序的协程执行它,它将崩溃。

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

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

CalledFromWrongThreadException:只有创建视图层次结构的原始线程才能触摸其视图。

✅ 以可以从任何协程调度程序执行的方式设计您的挂起函数。

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

现在我们可以从任何调度程序调用我们的登录函数。

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

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

避免使用全局范围

❌ 如果您在 Android 应用程序中到处使用 GlobalScope,您应该停止这样做。

GlobalScope.launch {
    // code
}

全局范围用于启动顶级协程,这些协程在整个应用程序生命周期内运行并且不会提前取消。

应用程序代码通常应使用应用程序定义的 CoroutineScope,强烈建议不要在 GlobalScope 实例上使用 async 或 launch。

✅ 在 Android 协程中,可以轻松地将范围限定为 Activity、Fragment、View 或 ViewModel 生命周期。

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
    }
}

转自:在使用 Kotlin 协程时应该注意的事情。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值