第八章 协程进阶:协程上下文与调度器、withContext、Flow基础、Channel基础、协程异常处理
协程基础咱们聊完了,现在该进入真正有意思的部分了。
说实话,刚学完协程基础那会儿,我也有点懵——启动协程是学会了,但怎么控制它跑在哪个线程?怎么在协程之间传递数据?出错了怎么办?
这些问题,就是本章要解决的。
8.1 协程上下文与调度器
协程上下文,说白了就是协程运行时的“环境配置包”。它里面装着各种信息,比如协程的名字、异常处理器、最重要的是——调度器。
调度器(Dispatcher)决定了协程跑在哪个线程上。Kotlin 提供了几个内置的调度器:
| 调度器 | 说明 | 典型场景 |
|---|---|---|
| Dispatchers.Main | 主线程,Android 上就是 UI 线程 | 更新 UI、与 View 交互 |
| Dispatchers.IO | 专为 IO 操作优化的线程池 | 网络请求、文件读写、数据库操作 |
| Dispatchers.Default | CPU 密集型任务的线程池 | 大数据排序、图片处理、JSON 解析 |
| Dispatchers.Unconfined | 不限制,在调用者的线程执行 | 极少用,特殊场景 |
来看个实际例子:
viewModelScope.launch(Dispatchers.IO) {
val data = apiService.fetchData() // 网络请求在 IO 线程
withContext(Dispatchers.Default) {
val processed = processData(data) // 数据处理在 Default 线程
}
withContext(Dispatchers.Main) {
uiState.value = processed // 更新 UI 切回主线程
}
}
你可能会问:为什么不直接用 launch 切换线程?嗯,这里有个关键点——launch 是启动一个新的协程,而 withContext 是在当前协程内切换上下文。后者更轻量,而且能保证执行顺序。
8.2 withContext 的妙用
withContext 是我个人非常喜欢的一个函数。它不会创建新协程,而是挂起当前协程,切换到指定上下文执行代码块,执行完再切回来。
它的返回值就是代码块最后一行的结果:
suspend fun loadUserProfile(): UserProfile = withContext(Dispatchers.IO) {
val user = userRepo.getUser() // IO 线程
val posts = postRepo.getPosts() // IO 线程
UserProfile(user, posts) // 返回结果
}
调用方不用关心线程切换,直接在主线程调用就行:
lifecycleScope.launch {
val profile = loadUserProfile() // 自动切到 IO,返回后切回 Main
showProfile(profile) // 主线程更新 UI
}
8.3 Flow 基础
Flow 是 Kotlin 协程体系里的“响应式数据流”。你可以把它想象成一个可以异步发射多条数据的序列。
为什么需要 Flow?
- LiveData 只能处理简单场景,不支持线程切换、背压
- RxJava 功能强大但学习成本高,API 复杂
- Flow 天然支持协程,轻量且强大
一个最简单的 Flow:
fun countDown(): Flow<Int> = flow {
for (i in 10 downTo 1) {
emit(i) // 发射数据
delay(1000) // 模拟每秒倒计时
}
emit(0) // 发射完成信号
}
// 收集数据
lifecycleScope.launch {
countDown().collect { value ->
textView.text = "倒计时: $value"
}
}
Flow 有三个关键操作:
- 构建:
flow { }、flowOf()、集合的.asFlow() - 中间操作:
map、filter、catch等,和集合操作类似 - 终端操作:
collect、first、toList等,触发数据流动
来看一个带错误处理的例子:
fun searchProducts(query: String): Flow<List<Product>> = flow {
emit(loadingState()) // 先发一个加载中状态
val result = apiService.search(query) // 网络请求
emit(successState(result)) // 发成功结果
}.catch { e ->
emit(errorState(e.message ?: "未知错误")) // 捕获异常,发射错误状态
}.flowOn(Dispatchers.IO) // 指定上游在 IO 线程执行
flowOn 改变了上游的调度器,但下游的 collect 仍然在调用它的协程上下文中执行。这个设计很巧妙——数据准备在后台,消费在主线程。
8.4 Channel 基础
Channel 是协程之间的通信管道。一个协程往里面放数据,另一个协程从里面取数据。有点像 BlockingQueue,但它是挂起的,不会阻塞线程。
Channel 有三种主要类型:
| 类型 | 行为 | 适用场景 |
|---|---|---|
| Rendezvous(默认) | 无缓冲,发送和接收必须同时发生 | 一对一同步通信 |
| Buffered | 有缓冲区,发送方可以提前放数据 | 生产速度 > 消费速度的场景 |
| Conflated | 只保留最新值,旧值被覆盖 | 只关心最新状态,如 UI 状态更新 |
来个简单的生产者-消费者例子:
val channel = Channel<String>(Channel.BUFFERED)
// 生产者
fun produce() = scope.launch {
for (i in 1..5) {
channel.send("消息 $i")
delay(200)
}
channel.close() // 发送完毕,关闭通道
}
// 消费者
fun consume() = scope.launch {
for (msg in channel) { // 自动遍历直到通道关闭
println("收到: $msg")
}
}
channel.close(),导致消费者协程一直挂起等待,内存泄漏。后来我养成了习惯——用 produce 协程构建器,它会自动管理关闭。
其实 produce 和 consumeEach 是更推荐的方式:
val channel = produce {
for (i in 1..5) {
send(i)
}
}
channel.consumeEach { value ->
println("消费: $value")
}
8.5 协程异常处理
协程里的异常处理,和普通代码不太一样。你想想看,协程可能跑在任意线程上,异常怎么传播?怎么捕获?
核心原则:未捕获的异常会沿着协程作用域向上传播,直到被处理或导致作用域取消。
处理方式有三种:
8.5.1 try-catch 包裹
最直接的方式,适合单个协程:
scope.launch {
try {
val data = apiService.fetchData()
showData(data)
} catch (e: Exception) {
showError(e.message)
}
}
8.5.2 CoroutineExceptionHandler
全局异常处理器,适合统一处理:
val handler = CoroutineExceptionHandler { _, exception ->
Log.e("Coroutine", "捕获到异常: ${exception.message}")
}
scope.launch(handler) {
throw RuntimeException("出错了")
}
launch 中有效。async 的异常需要通过 await() 时的 try-catch 捕获,或者使用 supervisorScope。
8.5.3 supervisorScope
这个我特别喜欢。它让子协程的失败不会影响兄弟协程:
supervisorScope {
val job1 = launch {
delay(100)
throw RuntimeException("任务1失败")
}
val job2 = launch {
delay(200)
println("任务2完成") // 这个会正常执行
}
}
如果是普通的 coroutineScope,job1 失败会导致 job2 也被取消。但 supervisorScope 不会——每个子协程独立处理自己的异常。
我在项目中用 supervisorScope 处理过并行下载的场景:一个文件下载失败,不影响其他文件继续下载。用户体验好很多。
8.6 本章知识体系
下面这张图,是我梳理的协程进阶知识结构:
这五个知识点,是协程进阶的核心。调度器控制线程,withContext 切换上下文,Flow 处理数据流,Channel 做协程间通信,异常处理保证稳定性。把它们串起来,你就能写出健壮的异步代码了。
我个人觉得,Flow 和 Channel 是这里面最值得花时间研究的。尤其是 Flow,它在 Android 开发中的地位越来越高,Room、Retrofit、DataStore 都在用。学会了,后面很多框架用起来就顺手了。
好了,协程进阶就聊到这儿。记住,多写多练,这些概念才能真正变成你自己的东西。