
前言
化神大圆满,你已驾驭 Flow 的异常天劫。冷流热流在你手中变幻自如,数据如长江大河,奔流不息。然而,你是否遇到过这样的场景?
你需要在两个协程之间传递数据 ------比如一个协程负责下载文件,另一个协程负责处理下载进度并更新 UI。你用
SharedFlow实现了,但总觉得哪里不对劲:SharedFlow是为了多播事件 设计的,你却在用它做一对一的任务传递 。更糟的是,你发现无法暂停上游生产------当 UI 来不及处理时,进度事件要么堆积,要么丢弃。
你需要的是一种更原始的、双向控制的 协程间通信机制------它允许一个协程发送 数据,另一个协程接收 数据;当接收方来不及处理时,发送方可以挂起等待 ;当没有数据时,接收方也可以挂起等待 。这正是 Kotlin 协程为你准备的管道 ------Channel。
Channel 是一个挂起式的、可缓冲的、协程间的通信原语 。它允许一个协程通过
send发送数据,另一个协程通过receive接收数据。Channel 是协程世界中的BlockingQueue,但它是非阻塞的------发送和接收都是挂起函数。
本讲是炼虚境的初阶修炼。你将:
- 彻底搞懂 Channel 的概念,以及它与 Flow/SharedFlow 的本质区别。
- 掌握 Channel 的四种容量策略:
RENDEZVOUS、BUFFERED、CONFLATED、UNLIMITED。 - 学会用
produce构建器简化生产者-消费者模式。 - 理解 Channel 的关闭与迭代。
- 在 Android 中用 Channel 实现一个优雅的下载队列。
准备好虚空造物,构筑你的第一条协程管道了吗?我们开始。
操千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意。
什么是 Channel?
在 Kotlin 协程的官方定义中:
Channel是一个非阻塞的、可挂起的、先进先出(FIFO)的通信原语 。它提供send和receive两个挂起函数,允许协程之间安全地传递数据。Channel 可以配置有限的容量,当缓冲区满时send挂起,当缓冲区空时receive挂起。
如果你熟悉 Java,可以将 Channel 理解为协程版的 BlockingQueue,但 send 和 receive 是挂起 而非阻塞线程。
kotlin
val channel = Channel<Int>()
// 生产者协程
launch {
for (i in 1..5) {
channel.send(i) // 发送数据,可能挂起
println("发送:$i")
}
channel.close() // 关闭通道
}
// 消费者协程
launch {
for (value in channel) { // 迭代接收,直到通道关闭
delay(1000)
println("接收:$value")
}
}
核心特性:
- 挂起而非阻塞 :
send在缓冲区满时挂起,receive在缓冲区空时挂起,线程不阻塞。 - 容量策略:可配置缓冲区大小及溢出行为。
- 可关闭 :
close()后不再接受新数据,接收方可消费剩余数据。 - 支持迭代 :
for (value in channel)自动处理关闭。
Channel vs Flow vs SharedFlow:三国演义
许多开发者初学 Channel 时会困惑:它和 Flow、SharedFlow 到底有什么区别?为什么有了 Flow 还要 Channel?
| 对比维度 | Channel | Flow(冷) | SharedFlow(热) |
|---|---|---|---|
| 设计目的 | 协程间通信 | 异步数据流计算 | 多播事件/状态 |
| 通信模式 | 点对点(可多个接收者,但每条数据只被一个接收) | 点对点(每个收集者独立流) | 点对多(所有订阅者收到相同数据) |
| 背压控制 | 缓冲区满时 send 挂起 |
拉取模型,消费者控制节奏 | 可配置缓冲与丢弃策略 |
| 历史重放 | 无 | 无(冷流) | 有 replay |
| 关闭 | 需要显式 close() |
无需,流自然结束 | 无需,但作用域取消时停止 |
| 典型场景 | 任务队列、Actor 模式 | 数据库查询、网络请求 | UI 状态、事件总线 |
简单选择法则:
- 需要协程间传递任务 ,且希望发送方可被背压挂起 →
Channel - 需要声明式数据流转换 ,且每次收集都重新执行 →
Flow - 需要多播事件或共享状态 ,且希望新订阅者收到历史 →
SharedFlow/StateFlow
Channel 的四种容量策略
Channel 的构造函数接受一个 capacity 参数,它决定了缓冲区的行为和 send 的挂起时机。
kotlin
val channel = Channel<Int>(capacity = Channel.RENDEZVOUS)
| 策略 | 容量 | 行为 | 适用场景 |
|---|---|---|---|
RENDEZVOUS(默认) |
0 | send 和 receive 必须会合------发送方挂起直到有接收方,反之亦然 |
无缓冲,严格同步 |
BUFFERED |
64(默认) | 缓冲区满时 send 挂起 |
典型的生产-消费队列 |
CONFLATED |
1(合并) | 缓冲区只保留最新 值,新值覆盖旧值,send 永不挂起 |
只关心最新状态(如位置) |
UNLIMITED |
无限 | send 永不挂起,需注意内存 |
生产者不受限,但消费者慢时可能 OOM |
自定义 Int |
指定值 | 缓冲区满时 send 挂起 |
精细控制内存与吞吐 |
示例:RENDEZVOUS 的会合行为
kotlin
val channel = Channel<Int>(Channel.RENDEZVOUS)
launch {
println("准备发送...")
channel.send(1) // 挂起,直到有接收方
println("发送完成")
}
launch {
delay(1000)
val value = channel.receive() // 此时发送方恢复
println("接收:$value")
}
// 输出顺序:准备发送... → (1秒后)发送完成 → 接收:1
示例:CONFLATED 的合并行为
kotlin
val channel = Channel<Int>(Channel.CONFLATED)
launch {
repeat(5) {
channel.send(it) // 永不挂起
println("发送:$it")
}
channel.close()
}
launch {
delay(500) // 模拟慢速消费者
for (value in channel) {
println("接收:$value")
}
}
// 可能输出:发送:0,1,2,3,4 → 接收:0 → 接收:4
// 中间的值被覆盖丢失了
produce 构建器:简化生产者协程
Kotlin 提供了 produce 协程构建器,它创建一个新的协程,返回一个 ReceiveChannel。生产者协程内部可以使用 send 发射数据,当协程结束时自动关闭 Channel。
kotlin
fun CoroutineScope.produceNumbers() = produce {
for (i in 1..5) {
delay(100)
send(i)
}
// 协程结束,自动 close()
}
fun main() = runBlocking {
val channel = produceNumbers()
channel.consumeEach { value ->
println(value)
}
}
produce 的优势:
- 自动管理 Channel 生命周期,无需手动
close()。 - 代码更紧凑,意图更清晰。
- 返回的
ReceiveChannel只能接收,类型安全。
Channel 的关闭与迭代
Channel 用完后需要关闭,否则接收方可能永远挂起。关闭后:
send会抛出ClosedSendChannelException。receive在缓冲区空时会抛出ClosedReceiveChannelException。- 使用
for循环迭代时会自动在关闭后退出。
安全迭代:consumeEach
consumeEach 是一个终端操作符,它迭代所有元素,并在完成后(或异常时)自动取消 Channel,确保资源释放。
kotlin
channel.consumeEach { value ->
println(value)
}
// 等价于手动 try-finally
关闭状态机
实战:用 Channel 实现下载队列
场景:用户可能同时点击多个文件下载,我们希望串行处理(一次只下载一个),并实时显示当前下载进度。Channel 是实现任务队列的完美选择。
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
class DownloadViewModel : ViewModel() {
sealed class DownloadEvent {
data class Progress(val url: String, val percent: Int) : DownloadEvent()
data class Complete(val url: String) : DownloadEvent()
}
// 任务队列:无缓冲,保证串行
private val downloadQueue = Channel<String>(Channel.RENDEZVOUS)
// 进度事件流(用 SharedFlow 多播给 UI)
private val _progressFlow = MutableSharedFlow<DownloadEvent>()
val progressFlow: SharedFlow<DownloadEvent> = _progressFlow.asSharedFlow()
init {
// 启动唯一的消费者协程,逐个处理下载任务
viewModelScope.launch {
for (url in downloadQueue) {
downloadFile(url)
_progressFlow.emit(DownloadEvent.Complete(url))
}
}
}
fun enqueueDownload(url: String) {
viewModelScope.launch {
downloadQueue.send(url) // 如果消费者正忙,这里会挂起排队
}
}
private suspend fun downloadFile(url: String) {
// 模拟下载,每 10% 报告一次进度
for (progress in 0..100 step 10) {
delay(500)
_progressFlow.emit(DownloadEvent.Progress(url, progress))
}
}
}
配合 Compose UI:
kotlin
@Composable
fun DownloadScreen(viewModel: DownloadViewModel = viewModel()) {
val events by viewModel.progressFlow.collectAsState(initial = null)
Column {
Button(onClick = { viewModel.enqueueDownload("file1.zip") }) {
Text("下载文件1")
}
Button(onClick = { viewModel.enqueueDownload("file2.zip") }) {
Text("下载文件2")
}
when (val event = events) {
is DownloadEvent.Progress -> Text("${event.url}: ${event.percent}%")
is DownloadEvent.Complete -> Text("${event.url} 完成!")
null -> Text("等待任务...")
}
}
}
设计要点:
- 使用
RENDEZVOUS确保任务严格串行,消费者处理完一个才接收下一个。 - 进度事件用
SharedFlow多播,UI 可灵活订阅。 send挂起自动形成等待队列,无需手动管理。
常见错误与避坑指南
错误 1:忘记关闭 Channel,导致接收方永久挂起
kotlin
val channel = Channel<Int>()
launch {
repeat(3) { channel.send(it) }
// 忘记 close()
}
for (value in channel) { // 接收完 0,1,2 后永久挂起
println(value)
}
正确 :发送完后 channel.close(),或用 produce 构建器自动关闭。
错误 2:在 CONFLATED Channel 上期望收到所有值
kotlin
val channel = Channel<Int>(Channel.CONFLATED)
repeat(10) { channel.send(it) }
// 消费者只能收到第一个和最后一个,中间丢失
正确 :只关心最新状态时才用 CONFLATED,否则用 BUFFERED。
错误 3:多个消费者同时从同一个 Channel 接收
kotlin
val channel = Channel<Int>()
launch { for (v in channel) println("A: $v") }
launch { for (v in channel) println("B: $v") }
// 每个值只会被一个消费者收到,且分配是不确定的
正确 :Channel 是点对点 通信,如需多播,用 SharedFlow 或 broadcast Channel。
错误 4:在 UI 层直接使用 Channel 的 receive 导致挂起
kotlin
// 错误:在 Composable 中直接调用挂起函数
@Composable
fun MyScreen() {
val value = viewModel.channel.receive() // 编译错误
}
正确 :通过 SharedFlow 或 StateFlow 暴露数据给 UI,Channel 仅作为内部通信。
最佳实践
- 优先使用
produce构建器:自动管理生命周期,代码更安全。 - 根据业务选择容量策略 :任务队列用
RENDEZVOUS或BUFFERED;状态更新用CONFLATED。 - 使用
consumeEach安全迭代:自动处理关闭和异常。 - Channel 用于内部通信,对外暴露 Flow:保持架构清晰。
- 注意 Channel 的线程安全性:Channel 本身是线程安全的,但发送的值应为不可变或线程安全对象。
- 配合
actor模式实现更复杂的状态管理(下一讲深入)。
总结与下回预告
恭喜,你已掌握 Channel 的虚空造物之术,炼虚境初阶修炼完成!
本讲核心收获:
Channel是协程间的挂起式通信管道,send/receive不阻塞线程。- 四种容量策略:
RENDEZVOUS(无缓冲)、BUFFERED(有缓冲)、CONFLATED(合并)、UNLIMITED(无限)。 produce构建器简化生产者协程,自动管理 Channel 生命周期。- Channel 需显式关闭,
consumeEach是安全迭代的推荐方式。 - Channel 用于点对点任务队列,多播场景应使用
SharedFlow。
在下一讲 【炼虚境·中阶】 中,我们将深入 Channel 的高级应用:BroadcastChannel 的兴衰、actor 模式实现并发安全、以及 select 表达式的多路复用。届时你会明白:
- 为什么
BroadcastChannel被废弃,SharedFlow如何取而代之? - 如何用
actor构建无锁的并发状态? select如何同时等待多个 Channel 或挂起函数?
【当前境界修为面板】
| 当前境界 | 修炼技能 | 修炼进度 | 修炼心得 |
|---|---|---|---|
| 炼虚境 · 初阶 | 1、Channel 管道术 2、容量四策 3、produce 构建诀 |
当前进度 :35% 修为 :350/1000 下一突破 :[炼虚境 · 中阶] (需领悟:BroadcastChannel 迁移、actor 模式、select 多路复用) |
Channel是协程间的BlockingQueue,但它不阻塞线程。send满则挂起,receive空则等待。 |
【本讲思考题】
-
表象题:以下代码的输出是什么?
kotlinval channel = Channel<Int>(Channel.CONFLATED) channel.send(1) channel.send(2) println(channel.receive()) println(channel.receive()) -
场景题:你需要在 ViewModel 中实现一个"消息中心",多个协程可以向它发送日志消息,一个专门的协程负责将日志批量写入文件(每次收集 10 条或每 5 秒写入一次)。应该使用什么 Channel 策略?写出核心结构。
-
原理题 :
Channel的send挂起时,协程是如何被"唤醒"的?请从AbstractSendChannel和协程调度器的角度简述其内部挂起与恢复机制。
道友,炼虚境的大门已敞开。掌握了 Channel 的高级应用,你的协程通信将更加灵动。炼虚境·中阶见。
欢迎一键四连 (
关注+点赞+收藏+评论)