Kotlin协程异常捕获陷阱:try-catch捕获异常失败了?

7 阅读1分钟

问题

线上有个崩溃日志,定位到的代码大致如下:

fun question(activity: FragmentActivity) {
    try {
        activity.lifecycleScope.launch {
            withContext(Dispatchers.IO) {
                //模拟在协程中抛异常
                throw IllegalStateException("Exception Occur")
            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
        //这里只会捕获函数本身的异常
        log("捕获Exception:$e")
    }
}

经过Tools -> Kotlin -> Show Kotlin Bytecode 反编译查看:

public final void question(@NotNull FragmentActivity activity) {
   Intrinsics.checkNotNullParameter(activity, "activity");

   try {
      BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope((LifecycleOwner)activity), (CoroutineContext)null, (CoroutineStart)null, new Function2((Continuation)null) {
         int label;

         public final Object invokeSuspend(Object $result) {
            Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch (this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  CoroutineContext var10000 = (CoroutineContext)Dispatchers.getIO();
                  Function2 var10001 = new Function2((Continuation)null) {
                     int label;

                     public final Object invokeSuspend(Object $result) {
                        IntrinsicsKt.getCOROUTINE_SUSPENDED();
                        switch (this.label) {
                           case 0:
                              ResultKt.throwOnFailure($result);
                              throw new IllegalStateException("Exception Occur");
                           default:
                              throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                        }
                     }

                     public final Continuation create(Object value, Continuation $completion) {
                        return (Continuation)(new <anonymous constructor>($completion));
                     }

                     public final Object invoke(CoroutineScope p1, Continuation p2) {
                        return ((<undefinedtype>)this.create(p1, p2)).invokeSuspend(Unit.INSTANCE);
                     }

                     // $FF: synthetic method
                     // $FF: bridge method
                     public Object invoke(Object p1, Object p2) {
                        return this.invoke((CoroutineScope)p1, (Continuation)p2);
                     }
                  };
                  Continuation var10002 = (Continuation)this;
                  this.label = 1;
                  if (BuildersKt.withContext(var10000, var10001, var10002) == var2) {
                     return var2;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            return Unit.INSTANCE;
         }

         public final Continuation create(Object value, Continuation $completion) {
            return (Continuation)(new <anonymous constructor>($completion));
         }

         public final Object invoke(CoroutineScope p1, Continuation p2) {
            return ((<undefinedtype>)this.create(p1, p2)).invokeSuspend(Unit.INSTANCE);
         }

         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object p1, Object p2) {
            return this.invoke((CoroutineScope)p1, (Continuation)p2);
         }
      }, 3, (Object)null);
   } catch (Exception e) {
      e.printStackTrace();
      CommonExtKt.log("捕获Exception:" + e);
   }
}

上述代码是CPS变换 + Continuation续体 + 状态机之后的结果,关于协程的使用,参见:深入理解Kotlin协程

在协程外部使用了try-catch,而 try-catch无法捕获协程内部抛出的异常,是因为协程内部的异常是通过协程的异常处理机制传播的,而不是同步地抛出到调用线程。解决办法通常是在协程内部使用try-catch,或者使用协程提供了的异常处理机制(如SupervisorJob、CoroutineExceptionHandler),协程的异常是传递性的,会传播给父协程或父Job,而不是直接在调用方被捕获。

try-catch是在调用协程函数时执行的,而函数内部的suspend函数是异步的,异常发生在协程启动后,try-catch可能已经执行完毕。注意:这里协程的"异步"指的是执行流程的时序控制,而不单指线程切换

解决方案

1、在协程内部使用try-catch

在协程内部处理:

/**
 * 方式1:try/catch放到协程内部
 */
fun solution1(activity: FragmentActivity) {
    activity.lifecycleScope.launch {
        try {
            withContext(Dispatchers.IO) {
                //模拟在协程中抛异常
                throw IllegalStateException("Exception Occur")
            }
        } catch (e: Exception) {
            e.printStackTrace()
            //这里只会捕获launch函数本身的异常
            log("捕获Exception:$e")
        }
    }
}

2、CoroutineExceptionHandler

全局异常处理器 CoroutineExceptionHandler:用于捕获未被子协程处理的异常,可以全局处理异常。

/**
 * CoroutineExceptionHandler处理异常
 */
fun solution2(activity: FragmentActivity) {
    val exceptionHandler = CoroutineExceptionHandler { context, throwable ->
        log("捕获Exception2:$throwable")
    }
    activity.lifecycleScope.launch(exceptionHandler) {
        //NOTE:这里可能被调度到将来某个时间点执行
        withContext(Dispatchers.IO) {
            //模拟在协程中抛异常
            throw IllegalStateException("Exception Occur")
        }
    }
}

总结:核心思想是将try-catch放在异常抛出的地方(即协程的内部),或者使用协程提供的结构化并发机制来管理异常