案例一
|
|
先上日志:
|
|
异常能被launch指定的handler所捕捉。 分析: launch启动的协程用到的coroutineScope是一个StandaloneCoroutine,withContext启动的协程对应的coroutineScope是一个DispatchedCoroutine。 在withContext中发生异常的时候,首先会回调到DispatchedCoroutine的resumeWith,最终会走到finalizeFinishingState方法,该方法里面会判断是否存在异常,如果有异常会调用cancelParent方法:
可以看到如果isScopedCoroutine为true的时候,cancelParent直接返回true,如果返回true,那么就不触发自己的上面的handleJobException,也就是把异常继续往上抛了。例子中也就是launch对应的StandaloneCoroutine。而在StandaloneCoroutine中不会去cancelParent,所以会把异常交给了handleJobException了,所以上面的launch中传入的CoroutineExceptionHandler能捕获到该异常。
总结:如果子job中在处理异常的时候,cancelParent中如果isScopedCoroutine为true的时候,则不触发自己的handleJobException,也就是把异常交给了父job,如果父job不处理该异常,则会程序崩溃。
案例二
上面代码如果把withContext中的异常通过try-catch住,父job就收不到该异常了: 子携程把异常catch住后,父协程的handler捕捉不到异常,并且父协程的invokeOnCompletion收不到异常,并且父协程之后的代码也能正常执行。
案例三
子协程给context传递coroutineExceptionHandler:
子协程抛了异常,然后子协程也传了CoroutineExceptionHandler,但是子协程的CoroutineExceptionHandler不起作用,还是把异常传给了父协程。并且父协程的invokeOnCompletion收到了异常回调,而且发现父协程的invokeSuspend方法也没走完,所以onCreate: parentJob end没有输出。
分析:前面已经分析过withContext开启的协程对应的coroutineScope是一个DispatchedCoroutine重写了isScopedCoroutine=true,如果它为true,cancelParent方法则返回true,那么它自己的handleJobException就不会触发,所以就不会走到自己的CoroutineExceptionHandler回调了。
案例四
当子协程抛的是CancellationException,父协程捕捉不到该异常:
此处handler没有捕捉到异常,并且程序也没崩溃,这是因为在子协程把异常回调给父协程后,父协程对应的scope,也就是StandaloneCoroutine在cancelParent中判断是CancelationException,它直接返回true,所以不会调用handleJobException方法,而向外抛异常的正是该方法。所以当子协程抛出CancellationException时候不会使父协程崩溃。
案例五
|
|
日志如下:
|
|
数据结构:父协程的job启动了三个子协程,在job1中抛出异常,job2和job3收到了JobCancellationException。其中父job是一个JobImpl对象,在每个子协程启动过程中都会创建一个ChildHandleNode对象,其中job指向了父Job,也就是JobImpl,childJob指向了当前子job,也就是StandaloneCoroutine,最后在子Job中通过parentHandle指向了父job,state指向了InvokeOnCompletion(是通过invokeOnCompletion添加的)。父job中通过state指向了一个NodeList,每一个next节点指向了三个子job。
异常处理:当第一个job发生异常后,会通知自己的state中的InvokeOnComplete接口,所以job1中收到了runtime exception的信息,它是原始的失败信息。接着会调用到job1的cancelParent逻辑,该方法中会调用到parentHandle的childCancelled逻辑,它是一个ChildHandleNode对象,在它的childCancelled方法中,会触发父job的childCancelled方法,最终会来到父job的cancelImpl方法。该方法里面会去取消子job,是通过遍历state节点,调用里面的每一个ChildHandleNode的invoke方法,在invoke里面会调用到childJob的parentCancelled方法,里面会给每一个job的state设置上JobCancellationException,当让第一个job由于已经有exception了,也就是原始exception,所以job2和job3会设置上JobCancellationException异常。接着子协程会递归调用notifyHandlers通知子协程发生异常了,直到没有子协程为止。 当所有的子协程处理完异常后,会调用到自己的CoroutineExceptionHandler的回调。当回调完后,会给自己的state添加异常,防止在resumeWith中往下执行逻辑。其中给state添加异常是通过Finishing.addExceptionLocked添加异常信息。
子协程invokeOnCompletion接口回调执行时机? 在异常处理阶段,父协程会调用到ChildHandleNode中的invoke方法,继而会触发childJob的parentCancelled方法,最终会在notifyHandlers中触发invokeOnCompletion的回调?
子协程的job为什么在添加完异常后resumeWith不会被执行? 在协程job中在执行resumeWith的时候,调用到tryMakeCompleting时,会判断state,如果不是Incomplete状态,则直接返回,不会往下执行。
父协程在cancelImpl的时候除了取消子协程还做了什么? 在取消完子协程的时候,给第一个child添加了ChildCompletion回调,该回调执行的时候会执行到continueCompleting,最终会回到自己处理异常的逻辑。
案例六
把案例五中的Job()换成SupervisorJob()
|
|
只是换了个job,job1还是一样收到了原始异常,job2和job3正常执行,并能执行完。按照上面分析,当job1发生异常的时候,会调用到cancelParent,它会分发到父job的childCancelled方法,而SupervisorJob重写了该方法直接返回false。所以异常不会分发到子协程中,当job1的cancelParent返回false的时候,会执行到handleJobException,而job1使用的context中的CoroutineExceptionHandler是使用的父job中指定的CoroutineExceptionHandler。因为context是plus叠加的方式。所以最后handler中收到了异常。
案例七
结果:try-catch竟然捕获不住,程序直接抛异常了
此处try-catch的位置不在子协程的SuspendInvoke位置,它是在主协程的launch位置,其实它是在invokeSuspend中调用内部launch的时候加了try-catch,对应的class代码如下:
从字节码来看,只是启动子协程的时候给try-catch,从原理上分析,当子协程发生异常后,会调用父协程的cancelChild,在父job里面会调用到cancelParent,而父job(shi 也就是JobImpl),它的parentHandle是空,所以cancelParent的方法返回false,因此会调用到handleJobException,而父job没有传handler,因此程序会崩溃。 上面的案例,其实可以理解为launch启动的协程中再启动一个lanuch协程,然后try-catch捕捉位置在外层lauch上,此时是捕捉不到异常的
案例八
结果:此处不会发生异常,异常被exceptionHandler捕捉 因此当父job接收到异常后,会将异常传递到handleJobException方法中,而父job是有handler的,所以将异常回调到了handler中,程序不会崩溃。
案例九
结果:程序崩溃了,不会被子协程的handler捕捉到异常
因为当子协程异常的时候,会把异常抛给了父job,而父job又没有处理该异常,所以程序崩溃了
案例十
结果:程序不会崩溃,异常被try catch捕获住,而不是被exceptionHandler捕获住。
前面分析过当子协程发生异常后,会把异常分发给到父job,在父job中需要等子job都处理完异常了,才会往下走,该处逻辑主要体现在子job中添加了一个ChildCompletion节点到state中,在invoke中会执行到parent.continueCompletion方法: 此处的parent实际是coroutineScope启动的协程对应的job,它是ScopeCoroutine,看下它的continueCompleting实现: 最终会把结果回调给到了传递进来的continuation,也就是最外层launch启动的时候的SuspendLambda,最终会调用它的invokeSuspend方法: 所以最终被try-catch捕捉到异常。
案例十一
结果:程序被内层launch指定的exceptionHandler捕捉了 supervisorScope用到的job是SupervisorCoroutine,它重写了childCancel方法,并返回false,所以当子job发生异常的时候,不会抛给父job,并执行自己的handleJobException方法,所以被自己的handler捕获到。
案例十二
此时被外层launch的handler所捕捉,原因是异常抛给了父job。
案例十三
此时被内层launch的handler所捕捉,原因是supervisorScope启动的协程不会往上抛,交给了子协程自己处理。