协程中的异常

    科技2022-08-01  120

    重点 (Top highlight)

    We, developers, usually spend a lot of time polishing the happy path of our app. However, it’s equally important to provide a proper user experience whenever things don’t go as expected. On one hand, seeing an application crash is a bad experience for the user; on the other hand, showing the right message to the user when an action didn’t succeed is indispensable.

    我们(开发人员)通常会花费大量时间来完善我们应用的快乐之路。 但是,同样重要的是,只要事情没有按预期进行,提供适当的用户体验。 一方面,看到应用程序崩溃对于用户是一种糟糕的体验; 另一方面,在操作未成功时向用户显示正确的消息是必不可少的。

    Handling exceptions properly has a huge impact on how users perceive your application. In this article, we’ll explain how exceptions are propagated in coroutines and how you can always be in control, including the different ways to handle them.

    正确处理异常会对用户如何感知您的应用程序产生巨大影响。 在本文中,我们将说明协程中如何传播异常以及如何始终控制异常,包括处理异常的不同方法。

    If you prefer video, check out this talk from KotlinConf’19 by Florina Muntenescu and I:

    如果您喜欢视频,请查看Florina Muntenescu和我在KotlinConf'19上的演讲 :

    ⚠️ In order to follow the rest of the article without any problems, reading and understanding Part 1 of the series is required.

    为了使本文的其余部分没有任何问题,请阅读并理解本系列的第1部分。

    一个协程突然失败了! 现在怎么办? 😱 (A coroutine suddenly failed! What now? 😱)

    When a coroutine fails with an exception, it will propagate said exception up to its parent! Then, the parent will 1) cancel the rest of its children, 2) cancel itself and 3) propagate the exception up to its parent.

    当协程发生异常而失败时,它将将该异常传播到其父级! 然后,父级将1)取消其其余子级,2)取消自身,3)将异常传播到其父级。

    The exception will reach the root of the hierarchy and all the coroutines that the CoroutineScope started will get cancelled too.

    异常将到达层次结构的根,并且CoroutineScope启动的所有协程也将被取消。

    An exception in a coroutine will be propagated throughout the coroutines hierarchy 协程中的异常将在整个协程层次结构中传播

    While propagating an exception can make sense in some cases, there are other cases when that’s undesirable. Imagine a UI-related CoroutineScope that processes user interactions. If a child coroutine throws an exception, the UI scope will be cancelled and the whole UI component will become unresponsive as a cancelled scope cannot start more coroutines.

    尽管在某些情况下传播异常可能是有道理的,但在其他情况下则是不希望的。 想象一下一个与UI相关的CoroutineScope ,它可以处理用户交互。 如果子协程抛出异常,则UI范围将被取消,并且整个UI组件将变得无响应,因为取消的范围无法启动更多协程。

    What if you don’t want that behavior? Alternatively, you can use a different implementation of Job, namely SupervisorJob, in the CoroutineContext of the CoroutineScope that creates these coroutines.

    如果您不想要这种行为怎么办? 另外,您可以在创建这些协程的CoroutineScope的CoroutineContext中使用Job的不同实现,即SupervisorJob 。

    主管求救 (SupervisorJob to the rescue)

    With a SupervisorJob, the failure of a child doesn’t affect other children. A SupervisorJob won’t cancel itself or the rest of its children. Moreover, SupervisorJob won’t propagate the exception either, and will let the child coroutine handle it.

    使用SupervisorJob ,一个孩子的失败不会影响其他孩子。 SupervisorJob不会取消自己或其他子项。 而且, SupervisorJob也不会传播该异常,并将让子协程处理该异常。

    You can create a CoroutineScope like this val uiScope = CoroutineScope(SupervisorJob()) to not propagate cancellation when a coroutine fails as this image depicts:

    您可以创建一个CoroutineScope这样val uiScope = CoroutineScope(SupervisorJob())当协程,因为这形象刻画失败不会传播取消:

    A SupervisorJob won’t cancel itself or the rest of its children SupervisorJob不会取消自身或其他子项

    If the exception is not handled and the CoroutineContext doesn’t have a CoroutineExceptionHandler (as we’ll see later), it will reach the default thread’s ExceptionHandler. In the JVM, the exception will be logged to console; and in Android, it will make your app crash regardless of the Dispatcher this happens on.

    如果未处理异常并且CoroutineContext没有CoroutineExceptionHandler (我们将在后面看到),它将到达默认线程的ExceptionHandler 。 在JVM中,异常将被记录到控制台。 在Android中,无论发生这种情况的Dispatcher,都会使您的应用崩溃。

    💥 Uncaught exceptions will always be thrown regardless of the kind of Job you use

    regardless无论您使用哪种Job,都会抛出未捕获的异常

    The same behavior applies to the scope builders coroutineScope and supervisorScope. These will create a sub-scope (with a Job or a SupervisorJob accordingly as a parent) with which you can logically group coroutines (e.g. if you want to do parallel computations or you want them to be or not be affected by each other).

    相同的行为适用于范围构建器coroutineScope和supervisorScope 。 这些将创建一个子范围(相应地具有Job或SupervisorJob作为父范围),您可以使用该子范围对协程进行逻辑分组(例如,如果要执行并行计算,或者希望它们相互影响或不相互影响)。

    Warning: A SupervisorJob only works as described when it’s part of a scope: either created using supervisorScope or CoroutineScope(SupervisorJob()).

    警告 : 仅当SupervisorJob属于范围的一部分时,其工作方式才可以描述:使用supervisorScope或CoroutineScope(SupervisorJob()) 。

    职位还是主管职位? 🤔 (Job or SupervisorJob? 🤔)

    When should you use a Job or a SupervisorJob? Use a SupervisorJob or supervisorScope when you don’t want a failure to cancel the parent and siblings.

    您什么时候应该使用Job或SupervisorJob ? 如果您不希望取消父级和同级项,请使用SupervisorJob或supervisorScope 。

    Some examples:

    一些例子:

    // Scope handling coroutines for a particular layer of my appval scope = CoroutineScope(SupervisorJob())scope.launch { // Child 1}scope.launch { // Child 2}

    In this case, if child#1 fails, neither scope nor child#2 will be cancelled.

    在这种情况下,如果child#1失败,范围也不是没有 child#2将被取消。

    Another example:

    另一个例子:

    // Scope handling coroutines for a particular layer of my appval scope = CoroutineScope(Job())scope.launch {supervisorScope { launch { // Child 1 } launch { // Child 2 } }}

    In this case, as supervisorScope creates a sub-scope with a SupervisorJob, if child#1 fails, child#2 will not be cancelled. If instead you use a coroutineScope in the implementation, the failure will get propagated and will end up cancelling scope too.

    在这种情况下,当supervisorScope使用SupervisorJob创建一个子作用域时,如果child#1失败,则不会取消child#2 。 相反,如果您在实现中使用coroutineScope ,则失败将传播并最终导致取消作用域。

    当心测验! 谁是我的父母? 🎯 (Watch out quiz! Who’s my parent? 🎯)

    Given the following snippet of code, can you identify what kind of Job child#1 has as a parent?

    给定以下代码段,您是否可以标识作为父级的Job child#1有哪些类型?

    val scope = CoroutineScope(Job())scope.launch(SupervisorJob()) { // new coroutine -> can suspend launch { // Child 1 } launch { // Child 2 }}

    child#1’s parentJob is of type Job! Hope you got it right! Even though at first impression, you might’ve thought that it can be a SupervisorJob, it is not because a new coroutine always gets assigned a new Job() which in this case overrides the SupervisorJob. SupervisorJob is the parent of the coroutine created with scope.launch; so literally, SupervisorJob does nothing in that code!

    child#1的parentJob类型为Job ! 希望你做对了! 即使乍一看,您可能已经认为它可以是SupervisorJob ,但这不是因为总是为新协程分配了新的Job() ,在这种情况下,该Job()覆盖了SupervisorJob 。 SupervisorJob是使用scope.launch创建的协程的父scope.launch ; 因此,从字面上看, SupervisorJob在该代码中不执行任何操作!

    The parent of child#1 and child#2 is of type Job, not SupervisorJob child#1和child#2的父项的类型为Job,而不是SupervisorJob

    Therefore, if either child#1 or child#2 fails, the failure will reach scope and all work started by that scope will be cancelled.

    因此,如果child#1或child#2失败,则该失败将到达范围,并且该范围开始的所有工作都将被取消。

    Remember that a SupervisorJob only works as described when it’s part of a scope: either created using supervisorScope or CoroutineScope(SupervisorJob()). Passing a SupervisorJob as a parameter of a coroutine builder will not have the desired effect you would’ve thought for cancellation.

    请记住 SupervisorJob 只要 属于作用域时,按说明进行工作:使用 supervisorScope 要么 CoroutineScope(SupervisorJob()) 。 将SupervisorJob作为协程生成器的参数传递将不会产生您希望取消的预期效果。

    Regarding exceptions, if any child throws an exception, that SupervisorJob won’t propagate the exception up in the hierarchy and will let its coroutine handle it.

    关于异常,如果有任何孩子抛出异常,则SupervisorJob不会在层次结构中向上传播该异常,并将让其协程对其进行处理。

    引擎盖下 (Under the hood)

    If you’re curious about how Job works under the hood, check out the implementation of the functions childCancelled and notifyCancelling in the JobSupport.kt file.

    如果您想了解如何Job的引擎盖下工作,检查出的功能的实现childCancelled和通知符 y 取消在JobSupport.kt文件。

    In the SupervisorJob implementation, the childCancelled method just returns false, meaning that it doesn’t propagate cancellation but it doesn’t handle the exception either.

    在SupervisorJob实现中, childCancelled方法仅返回false ,这意味着它不会传播取消,但也不会处理异常。

    处理异常👩‍🚒 (Dealing with Exceptions 👩‍🚒)

    Coroutines use the regular Kotlin syntax for handling exceptions: try/catch or built-in helper functions like runCatching (which uses try/catch internally).

    协程使用常规的Kotlin语法来处理异常: try/catch或内置帮助程序功能(如runCatching (内部使用try/catch ))。

    We said before that uncaught exceptions will always be thrown. However, different coroutines builders treat exceptions in different ways.

    我们之前说过, 总是会抛出未捕获的异常 。 但是,不同的协程构建器以不同的方式对待异常。

    发射 (Launch)

    With launch, exceptions will be thrown as soon as they happen. Therefore, you can wrap the code that can throw exceptions inside a try/catch, like in this example:

    通过启动, 异常一旦发生就会被抛出 。 因此,您可以将可能引发异常的代码包装在try/catch ,如以下示例所示:

    scope.launch { try { codeThatCanThrowExceptions() } catch(e: Exception) { // Handle exception }}

    With launch, exceptions will be thrown as soon as they happen

    启动后,异常一旦发生就会抛出

    异步 (Async)

    When async is used as a root coroutine (coroutines that are a direct child of a CoroutineScope instance or supervisorScope), exceptions are not thrown automatically, instead, they’re thrown when you call .await().

    当async用作根协程(协程是CoroutineScope实例或supervisorScope的直接子代)时, 不会自动引发异常,而是在调用 .await() 时引发异常 。

    To handle exceptions thrown in async whenever it’s a root coroutine, you can wrap the .await() call inside a try/catch:

    为了处理async只要它是根协程,就可以将.await()调用包装在try/catch :

    supervisorScope { val deferred = async { codeThatCanThrowExceptions() } try { deferred.await() } catch(e: Exception) { // Handle exception thrown in async }}

    In this case, notice that calling async will never throw the exception, that’s why it’s not necessary to wrap it as well. await will throw the exception that happened inside the async coroutine.

    在这种情况下,请注意,调用async将永远不会引发异常,这就是为什么也不必将其包装的原因。 await将引发async协程内部发生的异常。

    When async is used as a root coroutine, exceptions are thrown when you call .await

    当async用作根协程时,调用.await时会引发异常

    Also, notice that we’re using a supervisorScope to call async and await. As we said before, a SupervisorJob lets the coroutine handle the exception; as opposed to Job that will automatically propagate it up in the hierarchy so the catch block won’t be called:

    另外,请注意,我们正在使用supervisorScope调用async和await 。 如前所述, SupervisorJob让协程处理异常; 与Job相对,它将自动在层次结构中向上传播它,因此catch块不会被调用:

    coroutineScope { try { val deferred = async { codeThatCanThrowExceptions() } deferred.await() } catch(e: Exception) { // Exception thrown in async WILL NOT be caught here // but propagated up to the scope}}

    Furthermore, exceptions that happen in coroutines created by other coroutines will always be propagated regardless of the coroutine builder. For example:

    此外,由其他协程创建的协程中发生的异常将始终传播,而不管协程构建器如何。 例如:

    val scope = CoroutineScope(Job())scope.launch {async { // If async throws, launch throws without calling .await() }}

    In this case, if async throws an exception, it will get thrown as soon as it happens because the coroutine that is the direct child of the scope is launch. The reason is that async (with a Job in its CoroutineContext) will automatically propagate the exception up to its parent (launch) that will throw the exception.

    在这种情况下,如果async引发异常,则它将立即引发,因为作为作用域的直接子级的协程是launch 。 原因是async (在Job的CoroutineContext有一个async )将自动将异常传播到其父节点( launch ),从而引发异常。

    ⚠️ Exceptions thrown in a coroutineScope builder or in coroutines created by other coroutines won’t be caught in a try/catch!

    out️在coroutineScope生成器或其他协程创建的协程中引发的异常不会在try / catch中捕获!

    In the SupervisorJob section, we mention the existence of CoroutineExceptionHandler. Let’s dive into it!

    在SupervisorJob部分中,我们提到了CoroutineExceptionHandler的存在。 让我们开始吧!

    CoroutineExceptionHandler (CoroutineExceptionHandler)

    The CoroutineExceptionHandler is an optional element of a CoroutineContext allowing you to handle uncaught exceptions.

    该CoroutineExceptionHandler是一个可选元素CoroutineContext可让您处理未捕获的异常 。

    Here’s how you can define a CoroutineExceptionHandler, whenever an exception is caught, you have information about the CoroutineContext where the exception happened and the exception itself:

    您可以通过以下方法定义CoroutineExceptionHandler ,每当捕获到异常时,您都具有有关发生异常的CoroutineContext以及异常本身的信息:

    val handler = CoroutineExceptionHandler { context, exception -> println("Caught $exception")}

    Exceptions will be caught if these requirements are met:

    如果满足以下要求,将捕获异常:

    When ⏰: The exception is thrown by a coroutine that automatically throws exceptions (works with launch, not with async).

    当 ⏰时:协程会引发异常,协程会自动引发异常 (适用于launch ,不适用于async )。

    Where 🌍: If it’s in the CoroutineContext of a CoroutineScope or a root coroutine (direct child of CoroutineScope or a supervisorScope).

    其中 🌍:如果是在CoroutineContext一个的CoroutineScope或根协程(直接孩子CoroutineScope或supervisorScope )。

    Let’s see some examples using the CoroutineExceptionHandler defined above. In the following example, the exception will be caught by the handler:

    让我们来看一些使用CoroutineExceptionHandler定义的CoroutineExceptionHandler示例。 在以下示例中,异常将被处理程序捕获:

    val scope = CoroutineScope(Job())scope.launch(handler) { launch { throw Exception("Failed coroutine") }}

    In this other case in which the handler is installed in a inner coroutine, it won’t be caught:

    在处理程序安装在内部协同程序中的其他情况下, 不会被捕获:

    val scope = CoroutineScope(Job())scope.launch { launch(handler) { throw Exception("Failed coroutine") }}

    The exception isn’t caught because the handler is not installed in the right CoroutineContext. The inner launch will propagate the exception up to the parent as soon as it happens, since the parent doesn’t know anything about the handler, the exception will be thrown.

    由于未在正确的CoroutineContext安装处理程序,因此未捕获到异常。 内部启动会在异常发生后立即将异常传播到父级,因为父级对处理程序一无所知,因此将抛出异常。

    Dealing with exceptions gracefully in your application is important to have a good user experience, even when things don’t go as expected.

    在应用程序中优雅地处理异常对于获得良好的用户体验非常重要,即使事情并没有按预期进行。

    Remember to use SupervisorJob when you want to avoid propagating cancellation when an exception happens, and Job otherwise.

    要避免在发生异常时传播取消消息,否则请记住使用Job否则请记住使用SupervisorJob 。

    Uncaught exceptions will be propagated, catch them to provide a great UX!

    未捕获的异常将被传播,捕获它们以提供出色的UX!

    翻译自: https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c

    Processed: 0.015, SQL: 8