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

问题

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

kotlin 复制代码
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 反编译查看:

java 复制代码
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

在协程内部处理:

kotlin 复制代码
/**
 * 方式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:用于捕获未被子协程处理的异常,可以全局处理异常。

kotlin 复制代码
/**
 * 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放在异常抛出的地方(即协程的内部),或者使用协程提供的结构化并发机制来管理异常

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui