Android Kotlin(2) 协程上下文与异常处理机制

协程上下文CoroutineContext

1. CoroutineContext 是什么?

  • 本质 :一个键值对的集合 (类似于 Map),存储了协程运行时的配置信息。
  • 核心组件
    • 调度器(Dispatcher) :决定协程在哪个线程池运行(如 Dispatchers.MainDispatchers.IO)。
    • Job:控制协程的生命周期(取消、父子关系)。
    • 异常处理器(CoroutineExceptionHandler):处理未捕获的异常。
    • 协程名称(CoroutineName):调试时标识协程。
kotlin 复制代码
val context: CoroutineContext = Job() + Dispatchers.IO + CoroutineName("MyWorker")

记住 :每个协程都运行在一个 CoroutineContext


2. CoroutineContext 的关键特性

(1)组合与继承

  • 协程上下文可以通过 + 操作符组合:

    kotlin 复制代码
    val context = Dispatchers.IO + CoroutineName("MyCoroutine")
  • 子协程默认继承父协程的上下文(可覆盖部分元素)。

  • 如果两个元素有相同 Key(比如两个 Dispatcher),后面的会覆盖前面的

  • 这就是为什么你可以这样写:

kotlin 复制代码
    withContext(Dispatchers.IO) { ... } // 覆盖当前 dispatcher,但保留 Job 和其他元素

(2)层级结构

当你在一个协程内部启动新协程(子协程),子协程会继承父协程的上下文,但可以覆盖部分元素。

kotlin 复制代码
launch(Dispatchers.Main) { // 父协程:Main 线程 + 默认 Job
    println("Parent context: $coroutineContext")

    launch(Dispatchers.IO) { // 子协程
        // 继承了父 Job(所以能被父协程取消)
        // 但 Dispatcher 被替换为 IO
        println("Child context: $coroutineContext")
    }
}

输出结果:

text 复制代码
Parent context: [StandaloneCoroutine{Active}, Main]
Child context: [StandaloneCoroutine{Active}@1234, IO]

(3)协程上下文的继承公式

子协程上下文 = 父协程上下文 + 新指定的上下文

ini 复制代码
childContext = parentContext[Job]!!.plus(parentContext.minusKey(Job)) + customContext

简化理解就是:

  • Job 会被继承并建立父子关系
  • 其他元素(如 Dispatcher)可被覆盖
  • 如果你显式传入新的 Job(如 Job()),则断开父子关系(不推荐!)
javascript 复制代码
launch(Job()) { ... } // 创建了"孤儿协程",无法被父作用域管理!

协程异常处理

(1)学习协程异常处理的必要性

  • 默认情况下,任何一个子协程抛出未捕获异常,会立即取消整个协程作用域(包括兄弟协程)。
  • 在 Android 中,这可能导致 ViewModel 或 Activity 中所有后台任务突然停止。
  • 如果不处理,未捕获异常会 crash 应用 (虽然 CancellationException 不会)。

(2)自动传播异常 vs 向用户暴露异常

  • 自动传播 :协程框架默认将异常向上传播到作用域根部,若无人处理,则抛给 Thread.uncaughtExceptionHandler → App crash。
  • 向用户暴露:你主动捕获异常,转为 UI 提示(如 Toast、Snackbar)。
kotlin 复制代码
// ❌ 默认:crash
viewModelScope.launch {
    api.loadData() // 抛出 IOException → crash
}

// ✅ 主动暴露
viewModelScope.launch {
    try {
        api.loadData()
    } catch (e: IOException) {
        _uiState.value = ErrorState("网络错误")
    }
}

(3)非根协程的异常传播 & 异常的传播特性

核心规则:

作用域类型 子协程异常是否影响兄弟协程? 是否向上传播?
coroutineScope / 默认 Job ✅ 是 ✅ 是(取消整个作用域)
supervisorScope / SupervisorJob ❌ 否 ❌ 否(仅当前协程失败)
  • 普通 Job: "一荣俱荣,一损俱损"
  • SupervisorJob: "各自负责,互不影响"

(4)SupervisorJobsupervisorScope

SupervisorJob():创建一个"监督者" Job

scss 复制代码
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

scope.launch {
    throw RuntimeException("A failed") // 只取消自己
}

scope.launch {
    delay(1000)
    println("B still runs!") // ✅ 会执行
}

supervisorScope { }:临时作用域,内部协程互不影响

kotlin 复制代码
suspend fun fetchAll() = supervisorScope {
    val job1 = launch { fetchUser() }      // 失败不影响 job2
    val job2 = launch { fetchNotifications() }
    joinAll(job1, job2)
}

适用场景:并行加载多个独立数据源(如首页卡片),一个失败不应阻断其他。

(5)异常捕获的时机与位置 + 常见错误

正确做法:

  • 在抛出异常的协程内部 使用 try-catch
  • 或使用 CoroutineExceptionHandler
kotlin 复制代码
// 方式1:内部 try-catch(推荐)
launch {
    try {
        riskyOperation()
    } catch (e: Exception) {
        handle(e)
    }
}

// 方式2:全局异常处理器(适合兜底)
val handler = CoroutineExceptionHandler { _, exception ->
    logError(exception)
}
launch(handler) { riskyOperation() }

常见错误:

在外部函数 try-catch launch 块 → 无效!

php 复制代码
try {
    launch { throw Exception() } // ❌ 异常发生在另一个协程,这里 catch 不到
} catch (e: Exception) { ... }

supervisorScope 中忘记 catch,导致异常静默丢失

php 复制代码
supervisorScope {
    launch { throw Exception() } // ❌ 异常未被捕获,也不会 crash,但你不知道!
}

解决方案:即使使用 Supervisor,也要在每个 launch 内部处理异常。

(6)异常捕获阻止 App 闪退

在 Android 中,未处理的协程异常最终会调用 Thread.getDefaultUncaughtExceptionHandler(),导致 crash。

安全做法:

  • ViewModel 中 :每个 viewModelScope.launch 内部处理异常
  • 全局兜底(谨慎使用):
arduino 复制代码
// Application.onCreate()
Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
    // 上报 crash 日志
    // 注意:不要在这里启动新协程!
    defaultHandler.uncaughtException(thread, exception)
}

但更好的方式是:在协程层面就处理掉异常,而不是依赖全局 handler

(7)取消与异常

  • CancellationException 是特殊的
    • 它是协程取消的正常信号
    • 不会触发 CoroutineExceptionHandler
    • 不会导致应用崩溃
    • finally 块中仍可执行清理
scss 复制代码
launch {
    try {
        delay(1000)
    } finally {
        println("Cleanup") // 即使被 cancel() 也会执行
    }
}

不要把 CancellationException 当作错误处理!

(8)异常聚合(Exception Aggregation)

当多个子协程同时失败 ,协程框架会将它们包装成一个 AggregateException(实际是第一个异常,其他作为 suppressed exceptions)。

kotlin 复制代码
try {
    coroutineScope {
        launch { throw IOException("A") }
        launch { throw IllegalArgumentException("B") }
    }
} catch (e: Exception) {
    println(e) // 主异常是 "A"
    e.suppressed.forEach { println("Suppressed: $it") } // 包含 "B"
}

这在测试或批量任务中需要注意:不能只看主异常

总结

概念 关键点
CoroutineContext Job + Dispatcher + ExceptionHandler + Name;子协程继承父上下文
异常传播 默认"全家桶取消";SupervisorJob 实现"独立失败"
异常捕获 必须在协程内部 try-catch;外部 catch 无效
CancellationException 正常取消信号,不 crash,不触发 handler
Android 实践 ViewModel 中每个 launch 自己处理异常;慎用全局 handler
相关推荐
zhangphil6 小时前
Kotlin协程await与join挂起函数异同
kotlin
儿歌八万首7 小时前
Android 自定义 View 实战:打造一个跟随滑动的丝滑指示器
android·kotlin
4Forsee8 小时前
【Kotlin】Kotlin 基础语法:变量、控制和函数
kotlin
Propeller1 天前
【Kotlin】Kotlin 基础语法:变量、控制和函数
kotlin
Rysxt_1 天前
Kotlin前景深度分析:市场占有、技术优势与未来展望
android·开发语言·kotlin
莫白媛1 天前
Android开发之Kotlin 在 Android 开发中的全面指南
android·开发语言·kotlin
天勤量化大唯粉2 天前
基于距离的配对交易策略:捕捉价差异常偏离的均值回归机会(天勤量化代码实现)
android·开发语言·python·算法·kotlin·开源软件·策略模式
hudawei9962 天前
kotlin冷流热流的区别
android·开发语言·kotlin·flow··冷流·热流
hudawei9962 天前
对比kotlin和flutter中的异步编程
开发语言·flutter·kotlin·异步·