Kotlin中的Corutin模式和反模式

Kotlin中的Corutin模式和反模式


我决定写一些我认为在使用Kotlin协程时应该避免的事情。


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


如果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函数启动一个新的协程(1),该协程可能会引发未处理的异常。 如果尝试用try/catch (2) try/catch包装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显式启动异步协程调用时,此方法才有效。 因此,下面的代码仍将使您的应用程序崩溃,因为async是作为父协程(1)的一部分运行的。


 val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun loadData() = scope.launch { try { async { // (1) // may throw Exception }.await() } catch (e: Exception) { ... } } 

避免崩溃的另一种方法是更可取的,是将async包装在coroutineScope (1)中。 现在,当async内部发生异常时,它将取消在此区域中创建的所有其他协程,而不会触及外部区域。 (2)


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

另外,您可以在async块内处理异常。


使用主管理器进行根协程


如果需要进行后台工作并更新根协程内部的用户界面,请使用主调度程序启动它。


 val scope = CoroutineScope(Dispatchers.Default) // (1) fun login() = scope.launch { withContext(Dispatcher.Main) { view.showLoading() } // (2) networkClient.login(...) withContext(Dispatcher.Main) { view.hideLoading() } // (2) } 

在上面的示例中,我们使用CoroutineScope (1)中的CoroutineScope调度程序启动了根协程。 使用这种方法,每次我们需要更新用户界面时,我们都必须切换上下文(2)。


在大多数情况下,最好立即与主调度程序一起创建CoroutineScope ,这将导致代码的简化和较少的显式上下文切换。


 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意味着您要在后台运行多个协程,然后才等待它们。


避免取消工作


如果您需要取消协程,请不要取消作业。


 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函数,该函数的执行将取决于特定的协程管理器。


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

在上面的示例中,登录功能是暂停功能,如果从主调度程序将不使用的协程启动登录功能,它将失败。


 launch(Dispatcher.Main) { // (1)    val loginResult = login() ... } launch(Dispatcher.Default) { // (2)   val loginResult = login() ... } 

CalledFromWrongThreadException:仅创建View组件层次结构的源线程可以访问它们。

创建悬浮函数,以便可以从任何协程管理器执行该函数。


 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中使用异步启动

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

Source: https://habr.com/ru/post/zh-CN432942/


All Articles