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
相关推荐
小书房9 小时前
Kotlin的内联函数
java·开发语言·kotlin·inline·内联函数
zhangphil12 小时前
Android Page3与Flow分页查媒体数据库展示宫格图片列表,Kotlin
android·kotlin
胡致和1 天前
配置变更后,弹窗为什么飞到了最左边?
kotlin
zhangphil1 天前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
小书房1 天前
Kotlin使用体验及理解1
android·开发语言·kotlin
Kapaseker1 天前
我想让同事知道我很懂 Compose 怎么办?
android·kotlin
jinanwuhuaguo2 天前
OpenClaw工程解剖——RAG、向量织构与“记忆宫殿”的索引拓扑学(第十三篇)
android·开发语言·人工智能·kotlin·拓扑学·openclaw
jinanwuhuaguo2 天前
OpenClaw协议霸权——从 MCP 标准到意图封建化的政治经济学(第十八篇)
android·人工智能·kotlin·拓扑学·openclaw
zhangphil2 天前
Android sql查媒体数据封装room Dao构造AndroidViewModel,RecyclerView宫格展示,Kotlin
android·kotlin
jinanwuhuaguo2 天前
反熵共同体——OpenClaw的宇宙热力学本体论(第十七篇)
大数据·人工智能·安全·架构·kotlin·openclaw