学习 Kotlin 协程就像从 “命令式思维” 向 “声明式 / 异步思维” 的一次跳跃。它的核心魅力在于用同步的代码写出异步的效果。
以下是学习路线建议,以及对 SupervisorJob 的深度解析。
# 学习 Kotlin 协程的路线图
协程的学习不建议死磕源码,建议分三步走:
基础概念(挂起与恢复):搞清楚 suspend 关键字。理解 “挂起” 不等于 “阻塞”,它是让出线程控制权。
三要素(Scope, Context, Dispatcher):
Scope (作用域):控制协程的生命周期(如 viewModelScope)。
Context (上下文):协程的数据载体(包括 Job、Dispatcher、Handler)。
Dispatcher (调度器):规定协程跑在哪个线程(Main, IO, Default)。
异常与取消(核心难点):理解结构化并发。这就是你提到的 Job 和 SupervisorJob 的主战场。
数据流(Flow):学习如何处理响应式数据序列。
# Job vs. SupervisorJob
这是协程面试和实战中最常见的坑。它们的区别主要在于 **“异常的向上传递”**。
Standard Job (默认)
连坐机制:如果一个子协程失败了,它会立即把异常抛给父协程。
级联取消:父协程接收到异常后,会取消掉所有其他子协程,最后自己也挂掉。
适用场景:任务之间有强耦合,一个碎了,全盘皆输。
SupervisorJob (监督者)
各自安好:如果一个子协程失败了,它不会影响父协程,也不会影响其他的兄弟协程。
单向传递:异常只会在该子协程内部处理,或者传给特定的处理器,而不会导致整个作用域崩塌。
适用场景:任务之间相互独立。比如 UI 界面上有三个模块同步加载数据,其中一个失败了,另外两个应该继续显示。
# 拆解你的代码片段
你提供的这段代码是一个非常标准且稳健的 “全局 / 长生命周期作用域” 配置。
val myScope = CoroutineScope(
SupervisorJob() + // 1. 异常不连累他人
Dispatchers.IO + // 2. 默认跑在 IO 线程池
exceptionHandler + // 3. 最后的保底捕获器
CoroutineName("AppScope") // 4. 调试标签
)
各组件的作用:
SupervisorJob ():这是最关键的。它保证了如果你在这个 myScope 下启动了 10 个协程,其中一个网络请求崩了,剩下的 9 个依然能正常运行。
Dispatchers.IO:指定了默认线程。由于这个 Scope 通常处理后台任务,IO 线程池是最合适的。
exceptionHandler:这是 CoroutineExceptionHandler。
注意:在 SupervisorJob 的环境下,子协程的异常如果没被 try-catch,最终会由这个 exceptionHandler 处理。
如果没有这个 Handler,程序依然可能会崩溃(取决于平台底层处理机制)。
- CoroutineName:在日志调试时极其有用。通过 Thread.currentThread ().name 你能看到 AppScope,快速定位错误来源。
# 异常处理的 “潜规则”
在你的写法中,异常处理的逻辑是这样的:
当一个协程发生异常:
它会尝试查看自己是否有 try-catch。
如果没有,它会将异常交给父级的 SupervisorJob。
由于是 SupervisorJob,它不会取消其他兄弟。
最终,异常会交给 exceptionHandler 打印日志或上报错误。
错误的示范(新手最容易犯的错):
// ❌ 错误做法:直接在 launch 时传 SupervisorJob
scope.launch(SupervisorJob()) { ... }
这在 launch 内部是无效的,因为 launch 会创建一个新的 Job 覆盖掉你传进去的。SupervisorJob 必须定义在 CoroutineScope 的初始化中,或者作为顶级父 Job。