第 3 篇我们讲过 Kotlin 协程的入门用法。到了高级篇,光会 launch 和 withContext 已经不够了。面试官会追问:
Dispatchers.Default和Dispatchers.IO到底有什么区别?Job和SupervisorJob的差异是什么?协程异常为什么有时会吞掉,有时又会崩溃 App?
Flow的冷流和热流怎么选?
这篇会把协程的底层机制和异常处理模型讲清楚。
一、协程不是线程,但运行在线程上
很多人把协程理解成“轻量级线程”,这话对了一半。
准确说:协程是代码执行的“可挂起计算单元”,它可以被调度到线程上执行,也可以在不阻塞线程的情况下挂起。
1.1 挂起的本质
suspend fun fetchData(): String = withContext(Dispatchers.IO) {
// 网络请求
api.getData()
}
withContext 挂起时:
当前协程让出线程。
线程回去执行其他协程或事件循环。
IO 完成后,协程被恢复到可用线程继续执行。
线程没有被阻塞,只是协程被暂停了。
1.2 线程池模型
Kotlin 协程默认提供三种 Dispatchers:
注意:Default 线程数 = CPU 核心数,IO 线程数 = max(64, CPU核数 * 2)。
二、Dispatchers 的源码差异
2.1 Default 和 IO 共享线程池吗?
Kotlin 协程内部对 Default 和 IO 做了统一调度,但 IO 允许更多线程。
// 简化理解
internal object DefaultScheduler : Scheduler() {
val IO: CoroutineDispatcher = LimitingDispatcher(
corePoolSize = 0,
maxPoolSize = 64,
idleKeepAliveMs = 60_000L,
name = "Dispatchers.IO"
)
}
Dispatchers.IO 在任务量增大时会创建更多线程,避免 IO 等待阻塞整个线程池。
2.2 混淆点:IO 上能不能做 CPU 计算?
能,但不推荐。因为 Dispatchers.IO 设计目标是处理阻塞式 IO,不是密集型计算。大量计算应该放在 Dispatchers.Default。
2.3 Main.immediate 和 Main 的区别
// Dispatchers.Main.immediate
withContext(Dispatchers.Main.immediate) {
// 如果已经在主线程,不投递到 Looper,直接执行
}
withContext(Dispatchers.Main) {
// 即使已经在主线程,也会 post 到 Looper 队列尾部
}
Main.immediate 可以减少一次 Handler post,响应更快。但要注意:如果当前不在主线程,它和 Main 行为一致。
三、Job:协程的句柄与生命周期
每个 launch 或 async 都会返回一个 Job,它是协程的句柄。
3.1 Job 的状态机
New ──► Active ──► Completing ──► Completed
│
▼
Cancelling ──► Cancelled
isActive:是否正在运行。isCompleted:是否已完成(包括正常完成和取消)。isCancelled:是否被取消。
3.2 结构化并发
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
launch { task1() }
launch { task2() }
}
子 Job 会挂载到父 Job 下。父 Job 取消时,所有子 Job 都会被取消;所有子 Job 完成后,父 Job 才算完成。
3.3 SupervisorJob:子协程独立失败
val supervisor = SupervisorJob()
val scope = CoroutineScope(supervisor + Dispatchers.Main)
scope.launch {
launch {
throw RuntimeException("子协程 1 失败")
}
launch {
delay(100)
println("子协程 2 仍然执行")
}
}
SupervisorJob 下,一个子协程失败不会影响其他子协程。这是 ViewModelScope 默认使用 SupervisorJob 的原因:一个数据流失败不应该把整个 ViewModel 搞崩。
四、协程异常处理:为什么有时会崩溃?
协程异常处理是 Kotlin 协程里最容易踩坑的部分。
4.1 异常传播规则
4.2 为什么 launch 会崩溃 App?
CoroutineScope(Dispatchers.Main).launch {
throw RuntimeException("boom")
}
launch 的异常会向上冒泡,最终到达 CoroutineExceptionHandler。如果没有设置 handler,默认会交给线程的未捕获异常处理器,结果就是 App 崩溃。
4.3 正确处理异常
方案 1:try-catch
viewModelScope.launch {
try {
riskyOperation()
} catch (e: Exception) {
_error.value = e.message
}
}
方案 2:CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
Log.e("Coroutine", "未捕获异常: ${exception.message}")
}
val scope = CoroutineScope(handler + Dispatchers.Main)
scope.launch {
throw RuntimeException("这里会被 handler 捕获")
}
注意:CoroutineExceptionHandler 只能捕获 launch 抛出的未处理异常,不能捕获 async 里未 await 的异常。
4.4 async 的异常陷阱
val deferred = async {
throw RuntimeException("async 异常")
}
// 如果不调用 await(),异常会被暂存,不会立即抛出
只有调用 await() 时,异常才会抛出。这也是 async 更适合有返回值场景的原因。
五、Flow:响应式数据流的最佳实践
5.1 Flow 是冷流
val flow = flow {
emit(1)
emit(2)
}
// 每个 collect 都会重新执行 flow 块
flow.collect { println(it) }
flow.collect { println(it) }
每次 collect 都会重新执行 flow { } 代码块,这是“冷流”的定义。
5.2 StateFlow 和 SharedFlow 是热流
// StateFlow
private val _state = MutableStateFlow(UiState.Idle)
val state: StateFlow<UiState> = _state.asStateFlow()
// SharedFlow
private val _events = MutableSharedFlow<String>()
val events: SharedFlow<String> = _events.asSharedFlow()
5.3 Flow 线程切换
flow { emit(api.getData()) }
.flowOn(Dispatchers.IO) // 上游在 IO 线程执行
.onEach { updateUi(it) } // 下游默认仍在收集线程
.collect()
flowOn 只影响上游操作符的线程,下游由收集者决定。通常在 ViewModel 里收集时,viewModelScope.launch 默认就是 Main 线程。
5.4 shareIn 和 stateIn:把冷流转热
val result: StateFlow<Result> = repo.fetchData()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Result.Loading
)
WhileSubscribed(5000) 表示当最后一个订阅者消失后 5 秒再停止上游,避免配置变更时重复请求。
六、常见反模式
小结
协程的难点不在 API,而在理解它的调度模型和异常传播:
协程挂起不阻塞线程。
Dispatchers 的选择决定性能和正确性。
Job / SupervisorJob 控制生命周期和失败隔离。
异常处理要区分 launch 和 async 的行为。
Flow 适合数据流,StateFlow 适合 UI 状态,SharedFlow 适合事件。
下篇预告
下一篇《第 10 篇:Android 网络层架构:Retrofit + OkHttp + 协程的最佳实践》会把协程和网络层结合起来:
Retrofit 怎么优雅支持协程和错误处理。
OkHttp 拦截器链的设计与日志、重试、鉴权实现。
网络层如何封装 Result 类型,让 ViewModel 更干净。
参考阅读
Kotlin 协程官方文档
Roman Elizarov 博客:Coroutine Exception Handling
《Kotlin 协程实战指南》
上一篇文章:第 8 篇《Hilt + 依赖注入:从入门到架构层设计》