0赞
赞赏
更多好文
深度实战|源码级解析|附可运行测试代码|Android/JVM通用
协程让异步编程优雅,但异常处理稍有不慎——轻则静默失败,重则应用崩溃。本文直击10个高频陷阱,助你构建健壮协程系统!
🌪️ 为什么协程异常如此“狡猾”?
传统线程异常:UncaughtExceptionHandler 全局兜底
协程异常:作用域传播 + 结构化并发 + 上下文隔离 → 异常流向复杂如迷宫
💡 核心认知:
launch:异常立即抛出(需主动处理)async:异常延迟到await()(易被忽略)- 异常处理器仅对根协程生效(子协程需特殊处理)
- SupervisorJob ≠ Job(异常传播规则天壤之别)
⚠️ 陷阱1:launch中未捕获异常 → 整个作用域崩溃
❌ 错误示范
val scope = CoroutineScope(Job())
scope.launch {
throw RuntimeException("崩溃!")
}
scope.launch {
delay(100)
println("这行永远不会执行") // 作用域已取消!
}
现象:第一个协程抛异常 → 作用域内所有协程被取消(包括后续启动的)
✅ 正确方案(三选一)
// 方案1:局部try-catch(推荐简单场景)
scope.launch {
try { riskyWork() } catch (e: Exception) { log(e) }
}
// 方案2:SupervisorJob(子协程异常不波及其他)
val scope = CoroutineScope(SupervisorJob()) // 关键!
// 方案3:CoroutineExceptionHandler(全局兜底)
val handler = CoroutineExceptionHandler { _, e ->
FirebaseCrashlytics.recordException(e) // 上报监控
}
scope.launch(handler) { riskyWork() }
🔍 原理:普通
Job遵循“失败即取消”原则;SupervisorJob子协程相互隔离。
⚠️ 陷阱2:async异常被静默忽略(直到await())
❌ 致命错误
val deferred = scope.async {
throw IOException("网络失败")
}
// 无await()!异常永远不触发,内存泄漏风险!
现象:
- 无
await()→ 异常永久挂起(Deferred对象持有引用) - 有
await()但未try-catch → 异常在调用处抛出(易定位错误)
✅ 安全写法
// 方案1:立即await + try-catch
try {
val result = scope.async { apiCall() }.await()
} catch (e: Exception) { handle(e) }
// 方案2:使用async(start = CoroutineStart.LAZY) + 显式启动
val deferred = scope.async(start = CoroutineStart.LAZY) { apiCall() }
deferred.start() // 手动启动
// ...后续处理
💡 黄金法则:
async必须配await(),且await()需包裹异常处理!
⚠️ 陷阱3:try-catch包裹launch无法捕获内部异常
❌ 无效代码
try {
scope.launch { throw Exception("抓不到我!") } // 异常在协程内部抛出
} catch (e: Exception) {
println("永远进不来") // ❌ 无效!
}
原因:launch立即返回,异常在后续异步执行时抛出,try-catch已退出作用域
✅ 正确姿势
// 方案1:在launch内部try-catch(最直接)
scope.launch {
try { riskyWork() } catch (e: Exception) { ... }
}
// 方案2:使用await() + async(将异步转同步)
try {
scope.async { riskyWork() }.await()
} catch (e: Exception) { ... }
⚠️ 陷阱4:多个子协程异常 → 仅第一个被抛出
❌ 隐藏风险
supervisorScope {
launch { throw Exception("错误A") }
launch { throw Exception("错误B") } // 被抑制!
launch { throw Exception("错误C") } // 被抑制!
}
现象:仅"错误A"被抛出,B/C异常丢失(日志无记录,排查困难)
✅ 完整捕获方案
supervisorScope {
val exceptions = mutableListOf<Throwable>()
val jobs = listOf(
launch { try { work1() } catch (e: Throwable) { exceptions += e } },
launch { try { work2() } catch (e: Throwable) { exceptions += e } }
)
jobs.joinAll()
if (exceptions.isNotEmpty()) throw CompositeException(exceptions)
}
// 自定义复合异常(便于上报)
class CompositeException(val causes: List<Throwable>) : Exception() {
override val message: String = "多个异常: ${causes.size}个"
}
📌 Android实践:使用
kotlinx-coroutines-android的CoroutineExceptionHandler收集所有异常。
⚠️ 陷阱5:混淆Job与SupervisorJob的异常传播
| 场景 | Job(默认) | SupervisorJob |
|---|---|---|
| 子协程异常 | 取消所有兄弟协程 | 仅取消自身 |
| 适用场景 | 任务强关联(如事务) | 任务独立(如列表加载) |
| 陷阱 | 一个Item失败 → 整个列表加载中断 | 需手动处理每个异常 |
✅ 选择指南
// 场景:加载用户资料(头像+简介+动态)→ 任一失败整体失败
val scope = CoroutineScope(Job()) // 用Job
// 场景:首页多个卡片独立加载 → 一个失败不影响其他
val scope = CoroutineScope(SupervisorJob()) // 用SupervisorJob
⚠️ 陷阱6:非结构化并发中异常“消失”
❌ 危险写法
fun loadData() {
GlobalScope.launch { // ❌ 严禁使用GlobalScope!
apiCall() // 异常无作用域处理,可能静默失败
}
}
后果:
- 无父作用域 → 异常处理器失效
- 协程生命周期失控 → 内存泄漏
- Android中配置变更(旋转屏幕)导致重复请求
✅ 正确实践
// Android ViewModel中
class MainViewModel : ViewModel() {
private val viewModelScope = ViewModelScope() // androidx.lifecycle 2.1+
fun loadData() {
viewModelScope.launch { // 自动绑定生命周期
try { apiCall() } catch (e: Exception) { showError(e) }
}
}
}
🌐 通用方案:使用
lifecycleScope(Activity/Fragment)或自定义CoroutineScope绑定生命周期。
⚠️ 陷阱7:自定义CoroutineExceptionHandler被覆盖
❌ 无效设置
val scope = CoroutineScope(Job() + CoroutineExceptionHandler { _, _ ->
println("我的处理器")
})
scope.launch(CoroutineExceptionHandler { _, _ ->
println("父作用域的处理器会覆盖我!") // ❌ 不会执行
}) { ... }
规则:
- 子协程不能设置自己的
CoroutineExceptionHandler - 异常由父作用域的处理器处理
✅ 正确用法
// 全局设置(Application初始化时)
Thread.setDefaultUncaughtExceptionHandler { _, e ->
FirebaseCrashlytics.recordException(e)
}
// 作用域级设置(推荐)
val appScope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, e ->
when (e) {
is IOException -> retryNetwork()
else -> logAndReport(e)
}
})
⚠️ 陷阱8:Android主线程异常 → 应用崩溃
❌ 崩溃现场
lifecycleScope.launch(Dispatchers.Main) {
val data = withContext(Dispatchers.IO) { apiCall() }
textView.text = data // 若data为null → NullPointerException!
}
后果:主线程抛异常 → App直接闪退(用户感知极差)
✅ 防崩方案
lifecycleScope.launch {
try {
val data = withContext(Dispatchers.IO) { safeApiCall() }
textView.text = data ?: "加载失败"
} catch (e: Exception) {
showErrorToast("服务异常,请重试")
logError(e) // 上报监控
}
}
📱 Android专属:
- 使用
kotlinx-coroutines-android的MainScope()- 配合
lifecycle-runtime-ktx的repeatOnLifecycle管理生命周期
⚠️ 陷阱9:withContext切换上下文丢失异常上下文
❌ 隐蔽问题
launch {
try {
withContext(Dispatchers.IO) {
throw IOException("网络错误")
}
} catch (e: Exception) {
// 能捕获,但堆栈丢失原始协程上下文!
e.printStackTrace() // 难定位业务场景
}
}
痛点:异常堆栈缺失业务关键信息(如用户ID、操作步骤)
✅ 增强方案
// 方案1:自定义异常包装
class BusinessException(
val userId: String,
val operation: String,
cause: Throwable
) : Exception("用户[$userId]操作[$operation]失败", cause)
// 使用
withContext(Dispatchers.IO) {
try { apiCall() }
catch (e: IOException) {
throw BusinessException("U123", "加载首页", e)
}
}
// 方案2:使用StructuredLogging(推荐)
launch {
val context = mapOf("userId" to "U123", "page" to "Home")
try { ... }
catch (e: Exception) {
logger.error("业务异常", e, context) // 日志带上下文
}
}
⚠️ 陷阱10:Flow中未处理异常 → 整个流终止
❌ 流中断
flow {
emit(1)
throw IOException("中断!")
emit(2) // 永远不会执行
}.collect { println(it) } // 仅输出1,然后崩溃
后果:Flow遇到未处理异常 → 立即终止,后续数据丢失
✅ 安全收集方案
// 方案1:catch操作符(推荐)
flow { ... }
.catch { e ->
if (e is IOException) emit(-1) // 降级数据
else throw e // 非预期异常继续抛出
}
.collect { ... }
// 方案2:retryWhen(网络重试)
flow { apiCall() }
.retryWhen { cause, attempt ->
if (cause is IOException && attempt < 3) {
delay(1000 * attempt)
true // 重试
} else false
}
.catch { showError(it) }
.collect { ... }
💡 Flow黄金法则:
- 用
catch处理可恢复异常- 用
retry处理临时故障- 保留不可恢复异常向上抛出
🛡️ 协程异常处理最佳实践清单
| 场景 | 推荐方案 | 工具/库 |
|---|---|---|
| 全局兜底 | CoroutineExceptionHandler + 监控上报 | Firebase Crashlytics |
| 子协程隔离 | SupervisorJob + 局部try-catch | kotlinx-coroutines |
| async安全 | 必须配await() + try-catch | - |
| Android生命周期 | viewModelScope / lifecycleScope | androidx.lifecycle |
| Flow异常 | catch + retryWhen组合 | kotlinx-coroutines-flow |
| 异常上下文 | 自定义异常包装 + 结构化日志 | Timber, SLF4J |
| 测试验证 | runTest + assertThrows | kotlinx-coroutines-test |
🔚 结语:异常不是敌人,是系统的哨兵
协程异常处理的精髓不在“消灭异常”,而在:
🔹 精准捕获:知道异常该在哪里处理
🔹 清晰传播:让异常携带足够上下文
🔹 优雅降级:保障核心流程不中断
🔹 可观测性:每个异常都可追溯、可分析记住:
- 用
SupervisorJob隔离非关键任务async必配await()+异常处理- Android中永远绑定生命周期作用域
- Flow用操作符而非try-catch处理异常
🌱 行动建议:
1️⃣ 检查现有项目:是否有GlobalScope?async是否漏await?
2️⃣ 为ViewModel添加统一异常处理器
3️⃣ 在Flow中加入catch降级逻辑下期预告:《Kotlin Flow实战:从冷流到热流,构建响应式数据管道》
—— 深入Flow操作符、状态管理、与LiveData对比
