协程出错怎么办?别让崩溃找上门
写Android应用的时候,经常要从网络拉数据、操作数据库,这些活儿都得放在后台线程干。Kotlin协程用起来是真方便,但一旦中间出个异常,比如网络断了或者解析JSON出了问题,整个程序可能就卡住甚至直接崩掉。很多人刚开始用协程时,以为try-catch包一下就万事大吉,结果发现有些异常根本抓不到。
问题出在哪儿?协程是轻量级的,可以同时跑很多个,但它们之间的异常传播和普通线程不一样。如果你在一个launch启动的协程里抛了异常,默认情况下它不会像主线程那样被系统捕获,而是可能悄悄“吞掉”,只在日志里留下一行警告,用户那边就是界面没反应——这体验可太差了。
Job和CoroutineExceptionHandler配合使用
想要真正掌控协程里的异常,得靠CoroutineExceptionHandler。它就像一个全局的“异常收容所”,专门处理那些没人管的异常。不过要注意,它不能用在async里,因为async本身就会把异常包装成Result返回。
val handler = CoroutineExceptionHandler { _, exception ->
println("捕获到异常:$exception")
}
GlobalScope.launch(handler) {
throw RuntimeException("模拟异常")
}上面这段代码中,异常会被handler捕获并打印出来,而不是直接导致应用崩溃。这种写法适合用在那些不关心返回值、只负责执行任务的launch场景里。
子协程异常会向上蔓延
协程之间是有父子关系的。如果父协程没特别处理,子协程一出问题,整个结构都会被取消。比如你在一个页面启动了好几个协程去加载不同模块的数据,其中一个失败了,其他还在跑的任务也会被连带取消。
这种设计其实挺合理的。想象你在点外卖App刷新订单列表,有三个请求:订单数据、用户信息、优惠券状态。如果订单接口挂了,你还非得等另外两个跑完也没啥意义,干脆一起取消更高效。
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
throw RuntimeException("子任务失败")
}
// 其他协程不受影响
scope.launch {
println("这个还能正常执行")
}要是你不希望一个出错影响全部,就得换用SupervisorJob。它能让各个子协程独立运行,彼此的异常互不干扰。就像团队里每个人各干各的,谁摸鱼也不拖累别人。
async中的异常处理方式不同
async是用来获取结果的,它的异常不会直接往外抛,而是封装在Deferred对象里。你必须调用await()的时候才会真正触发异常。
val deferred = GlobalScope.async {
delay(1000)
throw IllegalArgumentException("参数不对")
}
try {
val result = deferred.await()
} catch (e: Exception) {
println("在这里捕获:$e")
}所以用async的时候,别忘了await()可能会抛异常,该包try-catch还得包。而且因为它是主动取结果,你可以选择重试、降级或者展示默认内容,控制权更灵活。
实际开发中的建议
在真实项目里,通常会把CoroutineExceptionHandler和全局日志监控结合起来。比如捕获到未处理异常时,除了打日志,还可以上报到崩溃分析平台,方便后续排查。
另外,不要在协程里随便忽略异常。哪怕只是打印一行log,也比什么都不做强。尤其是生产环境,静默失败最麻烦,用户说“打不开”,你查日志却啥都没有,那才叫头疼。