- 作者:老汪软件技巧
- 发表时间:2024-10-02 04:01
- 浏览量:
协程就是互相协作的程序,协程是结构化的。正因为 Kotlin 协程有这两个特点,这就导致它的异常处理机制与我们普通的程序完全不一样
在 Kotlin 协程当中,异常主要分为两大类,
之所以要这么分类,是因为在 Kotlin 协程当中,这两种异常的处理方式是不一样的。下面将围绕这个展开介绍
面试官的题库,让一部分人先拿Offer!
为什么 cancel() 不起作用?
当协程任务被取消的时候,它的内部是会产生一个 CancellationException 的。而协程的结构化并发,最大的优势就在于:如果我们取消了父协程,子协程也会跟着被取消。但是我们也知道,很多初学者都会遇到一个问题,那就是协程无法被取消。
这里,主要涉及了三个场景,我们一个个来分析下
场景1 cancel() 不被响应
但通过运行的结果,我们可以看到协程并不会被取 消。这是为什么呢?
协程是互相协作的程序。因此,对于协程任务的取消,也是需要 互相协作的。协程外部取消,协程内部需要做出响应才行。
具体来说,我们可以在协程体中加入状态判断:
进一步分析代码段 1 无法取消的原因了:当我们调用 job.cancel() 以后,协程任务已经不是活跃状态了,但代码并没有把 isActive 作为循环条件,因此协程无法真正取消。
到这里,我们就可以总结出协程异常处理的第一准则:****协程的取消需要内部的配合。
场景2:结构被破坏
我们都知道,协程是结构化的,当我们取消父协程的时候,子协程也会跟着被取消
但在某些情况下,我们嵌套创建的子协程并不会跟随父协程一起取消,比如下面这个
以上代码中,我们创建了一个 fixedDispatcher,它是由两个线程的线程池实现的。接着,我们通过 launch 创建了三个协程,其中 parentJob 是父协程,随后我们等待 2000 毫秒,然后取消父协程。
不过,通过程序的运行结果,我们发现,虽然“子协程 1”当中使用了 while(isActive) 作为判断 条件,它仍然无法被取消。其实,这里的主要原因还是在注释 1 处,我们在创建子协程的时候,
使用了 launch(Job()){}。而这种创建方式,就打破了原有的协程结构。新的结构关系如下图所示
可以看到“子协程 1”已经不是 parentJob 的子协程了,而对应的,它的父 Job 是我们在 launch 当中传入的 Job() 对象。所以,在这种情况下,当我们调用 parentJob.cancel() 的时候,自然也就无法取消“子协程 1”了
其实这个时候,如果我们稍微改动一下上面的代码,不传入 Job(),程序就可以正常取消了。此时结构关系如下图所示
到这里,我们就可以总结出第二条准则了:不要轻易打破协程的父子结构!
场景 3:未正确处理 CancellationException
对于 Kotlin 提供的挂起函数,它们是可以自动响应协程的取消的,比如说,当我们把 Thread.sleep(500) 改为 delay(500) 以后,我们就不需要在 while 循环当中判断 isActive 了
我们可以在以上代码的基础上,增加一个 try-catch。
注释 1,在用 try-catch 包裹了 delay() 以后,我们就可以在输出结果中,看到“CatchCancellationException”,这就说明 delay() 确实可以自动响应协程的取消,并且产生CancellationException 异常
不过在这段代码中,我们把“throw e”这行代码注释掉,重新运行之后,程序就永远无法终止 了。这主要是因为,我们捕获了 CancellationException 以后没有重新抛出去,就导致子协程 无法正常取消
我们就可以总结出第三条准则了:捕获了 CancellationException 以后,要考 虑是否应该重新抛出来。
好,到这里,我们就通过协程取消异常的三个场景,总结了三条准则,来应对 CancellationException 这个特殊的异常。 那么接下来,我们再来看看如何在协程当中处理普通的异常。
为什么 try-catch 不起作用?场景4:try-cath 范围不对
在 Kotlin 协程当中,try-catch 并非万能的。有时候,即使你用 try-catch 包裹了可能抛异常的代码,软件仍然会崩溃。
从运行结果这里,我们可以看到,try-catch 并没有成功捕获异常,程序等待了 100 毫秒 左右,最终还是崩溃了
这主要就是因为,当协程体当中的“1/0”执行的时候,我们的程序已经跳出 try-catch 的作用域了。
对于代码段 8 来说,我们可以挪动一下 try-catch 的位 置,比如说这样
我们就可以总结出第四条准则了:****不要用 try-catch 直接包裹 launch、 async
场景5:使用 SupervisorJob控制异常传播的范围。
SupervisorJob() 其实不是构造函数,它只是一个普通的顶层 函数。而这个方法返回的对象,是 Job 的子类
SupervisorJob 与 Job 最大的区别就在于,当它的子 Job 发生异常的时候,其他的子 Job 不会受到牵连,具体如下:
普通 Job,对于子 Job 出现异常时的应对策略。可以看到,由于 parentJob 是一个普通的 Job 对象,当 job1 发生异常之后,它会导致 parentJob 取消,进而导致 job2、job3 也受到牵连。
而这时候,如果我们把 parentJob 改为 SupervisorJob,job1 发生异常的的话,就不会影响到其他的 Job 了
我们就可以总结出第五条准则了:****灵活使用 SupervisorJob,控制异常传播的范围
不过并非所有情况下,我们都应该使用 SupervisorJob,有时候 Job 会更合适,这要结合实际场景分析。
场景6:CoroutineExceptionHandler
CoroutineExceptionHandler,它是 CoroutineContext 的元素之一,我们在创建协程的时候,可以指定对应的CoroutineExceptionHandler。
那么 CoroutineExceptionHandler 究竟适用于什么样的场景呢
在上面的代码中,我模拟了一个复杂的协程嵌套场景。对于这样的情况,我们其实很难一个个在每个协程体里面去写 try-catch。所以这时候,为了捕获到异常,我们就可以使用CoroutineExceptionHandler 了
我们定义了一个 CoroutineExceptionHandler,然后把它传入了 scope 当中,这样一来,我们就可以捕获其中所有的异常了
不过CoroutineExceptionHandler 需要定义在顶层,直接定义在发生异常的地方却不会生效!!!
上述代码CoroutineExceptionHandler直接定义在崩溃位置 反而无效
这是由于 当子协程当中出现异常以后,它们都会统一上报给顶层的父协程,然后顶层的父协程才会去调用 CoroutineExceptionHandler,来处理对应的异常
我们就可以总结出第六条准则了:使用 CoroutineExceptionHandler 处理复杂 结构的协程异常,它仅在顶层协程中起作用。
小结
在 Kotlin 协程当中,异常主要分为两大类,一类是协程取消异常(CancellationException),另一类是其他异常。为了处理这两大类问题,我们一共总结出了 6 大准则,这些我们都要牢记 在心。
第一条准则:协程的取消需要内部的配合。第二条准则:不要轻易打破协程的父子结构!这一点,其实不仅仅只是针对协程的取消异 常,而是要贯穿于整个协程的使用过程中。我们知道,协程的优势在于结构化并发,它的许 多特性都是建立在这个特性之上的,如果我们无意中打破了它的父子结构,就会导致协程无 法按照预期执行。第三条准则:捕获了 CancellationException 以后,要考虑是否应该重新抛出来。在协程 体内部,协程是依赖于 CancellationException 来实现结构化取消的,有的时候我们出于某 些目的需要捕获 CancellationException,但捕获完以后,我们还需要思考是否需要将其重 新抛出来第四条准则:不要用 try-catch 直接包裹 launch、async。这一点是很多初学者会犯的错 误,考虑到协程代码的执行顺序与普通程序不一样,我们直接使用 try-catch 包裹 launch、 async,是不会有任何效果的。第五条准则:灵活使用 SupervisorJob,控制异常传播的范围。SupervisorJob 是一种特殊 的 Job,它可以控制异常的传播范围。普通的 Job,它会因为子协程当中的异常而取消自 身,而SupervisorJob 则不会受到子协程异常的影响。在很多业务场景下,我们都不希望子 协程影响到父协程,所以 SupervisorJob 的应用范围也非常广。比如说 Android 当中的 viewModelScope,它就使用了 SupervisorJob,这样一来,我们的 App 就不会因为某个子 协程的异常导致整个应用的功能出现紊乱第六条准则:使用 CoroutineExceptionHandler 处理复杂结构的协程异常,它仅在顶层协 程中起作用。我们都知道,传统的 try-catch 在协程当中并不能解决所有问题,尤其是在协 程嵌套层级较深的情况下。这时候,Kotlin 官方为我们提供了 CoroutineExceptionHandler 作为补充。有了它,我们可以轻松捕获整个作用域内的所有异常。