Channel 和 Flow 都是 Kotlin 协程中处理异步数据流的工具,但它们的设计理念和使用场景有很大不同。
对比
| 特性 | Channel | Flow |
|---|---|---|
| 数据发射 | 热数据流 | 冷数据流 |
| 多消费者 | 共享(每个元素只被一个消费者接收) | 独立(每个消费者重新发射数据) |
| 背压处理 | 通过缓冲区策略 | 通过操作符(buffer、conflate等) |
| 状态 | 有状态,独立于消费者存在 | 无状态,依赖消费者存在 |
| 使用场景 | 事件总线、任务队列、通信 | 数据转换、响应式编程、UI状态 |
解释与示例
1. 热数据流 vs 冷数据流
热流、冷流的主要区别在于数据流的生产与消费之间的关系:
- 热流 :即使没有收集者(Collector)在监听,数据生产者也会立即开始生成数据。数据是共享的,新的收集者加入时只会收到其订阅之后发出的数据(除非设置了重放机制如StateFlow 的初始值)。Channel 就像一个管道,数据被推送到管道中,等待被接收者获取。
- 冷流 :数据生产者只在有收集者开始收集时才执行生产逻辑。每次有新的收集者,都会从头开始重新生成一份完整的数据流,实现一对一的按需生产。常规的 Flow 就是典型的冷流,当然也可以转换成SharedFlow,、StateFlow热数据流。
总结来说:
| 特性 | 冷流 (Flow) | 热流 (Channel, SharedFlow, StateFlow) |
|---|---|---|
| 生产时机 | 有收集者时才生产 | 独立于收集者,立即生产 |
| 数据共享 | 不共享,一对一 | 共享,一对多 |
| 生命周期 | 绑定到收集协程的生命周期 | 独立存在,需要手动取消或绑定到外部作用域 |
Channel(热数据流)
kotlin
val channel = Channel<Int>(Channel.UNLIMITED)
// 数据立即开始发射,不管有没有消费者
activity.lifecycleScope.launch {
launch {
repeat(3) { i ->
channel.send(i)
log("发送数据: $i")
delay(100)
}
channel.close()
}
delay(250) //延迟创建消费者
launch {
for (value in channel) {
log("收到数据: $value")
}
}
}
执行结果:
makefile
16:01:30.336 E 发送数据: 0
16:01:30.438 E 发送数据: 1
16:01:30.539 E 发送数据: 2
16:01:30.586 E 收到数据: 0
16:01:30.587 E 收到数据: 1
16:01:30.587 E 收到数据: 2
Channel构造方法中,capacity表示容量策略,通常有以下几种选择:
| 容量类型 | 发送行为 | 接收行为 | 适用场景 |
|---|---|---|---|
| RENDEZVOUS,默认值0 | 无缓冲区时挂起 | 无数据时挂起 | 严格同步的生产者消费者 |
| CONFLATED ,值=-1 | 永不挂起,覆盖旧值 | 正常接收 | 状态更新,只关心最新值 |
| BUFFERED ,值=-2 | 缓冲区满时挂起 | 正常接收 | 一般事件处理,应对突发 |
| UNLIMITED,值=Int.MAX_VALUE | 永不挂起 | 正常接收 | 绝对不能丢失数据的场景 |
| 固定数值 | 缓冲区满时挂起 | 正常接收 | 需要精确控制内存的场景 |
示例代码中使用的UNLIMITED,发送不会被挂起,如果上述代码中改成:
kotlin
val channel = Channel<Int>(Channel.RENDEZVOUS) //或者不传,默认就是RENDEZVOUS
//...其他不变...
设置RENDEZVOUS没有缓冲区,当没有接收时会挂起,所以发送跟接收是交替执行的,执行结果:
makefile
16:06:56.175 E 收到数据: 0
16:06:56.175 E 发送数据: 0
16:06:56.277 E 收到数据: 1
16:06:56.277 E 发送数据: 1
16:06:56.380 E 收到数据: 2
16:06:56.380 E 发送数据: 2
Flow(冷数据流)
kotlin
activity.lifecycleScope.launch {
val flow = flow {
repeat(3) { i ->
log("发射: $i")
emit(i)
delay(100)
}
}
delay(250) // 延迟消费
//每个消费者都会触发新的数据流
launch {
flow.collect { value ->
log("消费者1收到: $value")
}
}
launch {
delay(100)
flow.collect { value ->
log("消费者2收到: $value")
}
}
}
执行结果:
makefile
15:07:51.482 E 发射: 0
15:07:51.483 E 消费者1收到: 0
15:07:51.583 E 发射: 1
15:07:51.583 E 消费者1收到: 1
15:07:51.584 E 发射: 0
15:07:51.584 E 消费者2收到: 0 ← 消费者2重新开始
15:07:51.684 E 发射: 2
15:07:51.684 E 消费者1收到: 2
15:07:51.685 E 发射: 1
15:07:51.685 E 消费者2收到: 1
15:07:51.786 E 发射: 2
15:07:51.786 E 消费者2收到: 2
2. 多消费者行为
Channel 元素竞争,存在多个接收者时可能会交替接收数据,示例:
kotlin
activity.lifecycleScope.launch {
val channel = Channel<String>()
launch {
listOf("A", "B", "C").forEach {
channel.send(it)
}
channel.close()
}
// 多个消费者竞争同一个元素
repeat(2) { index ->
launch {
for (item in channel) {
log("消费者$index 收到: $item")
delay(100)
}
}
}
}
执行结果:
makefile
16:21:45.948 E 消费者0 收到: A
16:21:45.949 E 消费者1 收到: B
16:21:46.049 E 消费者0 收到: C
可以看到多个接收者时可能会交替接收发送者的数据。
Flow 独立数据流
kotlin
activity.lifecycleScope.launch {
val flow = flow {
listOf("A", "B", "C").forEach {
emit(it)
delay(100)
}
}
//每个消费者获得完整的数据副本
repeat(2) { index ->
launch {
flow.collect { item ->
log("消费者$index 收到: $item")
}
}
}
}
执行结果:
makefile
16:35:57.939 E 消费者0 收到: A
16:35:57.939 E 消费者1 收到: A
16:35:58.042 E 消费者0 收到: B
16:35:58.043 E 消费者1 收到: B
16:35:58.143 E 消费者0 收到: C
16:35:58.143 E 消费者1 收到: C
因为Flow是冷流,会在collect时才开始发送接收数据,所以可以看到每个接收者都获得了完整的数据。
3. 背压处理对比
Channel 背压
kotlin
// 通过缓冲区策略处理背压
val channel = Channel<Int>(
capacity = Channel.BUFFERED,
onBufferOverflow = BufferOverflow.SUSPEND // 缓冲区满时挂起
)
// 或者使用 CONFLATED 丢弃旧值
val conflatedChannel = Channel<Int>(Channel.CONFLATED)
// 如果使用UNLIMITED,那么缓冲区将无限长,如果发送快接收慢,发送的数据可以一直被缓存起来,不过也存在副作用,当发送数据过多时可能会出现内存问题
val unlimitedChannel = Channel<String>(Channel.UNLIMITED)
Flow 背压
kotlin
activity.lifecycleScope.launch {
flow {
for (i in 1..10) {
emit(i)
}
}
.buffer(10) // 添加缓冲区
.collect { value ->
delay(500) // 慢消费者
log("接收 $value")
}
}
执行结果:
makefile
17:22:13.898 E 接收 1
17:22:14.400 E 接收 2
17:22:14.904 E 接收 3
17:22:15.408 E 接收 4
17:22:15.912 E 接收 5
17:22:16.417 E 接收 6
17:22:16.921 E 接收 7
17:22:17.424 E 接收 8
17:22:17.927 E 接收 9
17:22:18.431 E 接收 10
buffer() 的目的是提高性能和吞吐量,通过允许生产者和消费者并行运行来实现,会在执行期间为流创建一个单独的协程。具体使用参见:Android Kotlin之Flow数据流。
上述示例中buffer()如果改成conflate(),则只会处理最新值,执行结果变成:
makefile
17:16:45.248 E 接收 1
17:16:45.749 E 接收 20
4. 互相转换
Channel 转 Flow
kotlin
val channel = Channel<String>()
val flow = channel.consumeAsFlow()
//将冷流转换为热流SharedFlow,这样就变成了一对多;否则是一对一,collect只能被调用一次。
.shareIn(
scope = activity.lifecycleScope, // 绑定的生命周期作用域
started = SharingStarted.Eagerly, // 立即启动共享流
replay = 0 // 新的消费者不需要重放历史数据
)
activity.lifecycleScope.launch {
launch {
listOf("A", "B", "C").forEach {
channel.send(it)
}
channel.close()
}
launch {
flow.collect { log("消费者1: $it") }
}
launch {
flow.collect { log("消费者2: $it") } // 可能收不到数据
}
}
执行结果:
makefile
17:42:38.510 E 消费者1: A
17:42:38.510 E 消费者1: B
17:42:38.510 E 消费者2: A
17:42:38.511 E 消费者2: B
17:42:38.512 E 消费者1: C
17:42:38.512 E 消费者2: C
Flow 转 Channel
kotlin
val flow = flow { /* ... */ }
//将冷流转为热流
val channel: ReceiveChannel = flow.produceIn(CoroutineScope(Dispatchers.Default))
不过该转换基本不用了,Flow已经被设计为处理异步数据流的主要API。所以绝大多数场景下,Flow的声明性、结构化并发和丰富的操作符更适合。
如何选择
使用 Channel
- Channel 代表 热流 的底层实现,类似于一个阻塞队列(Blocking Queue)或管道。数据发送者独立于接收者运行。
- 底层并发:需要一个底层的、线程安全的队列来进行协程间的通信。示例:Kotlin协程并发控制:多线程环境下的顺序执行
- 多个发送者/多个接收者: 当需要多个独立的协程向同一个管道发送数据,并且多个协程从同一个管道竞争性地接收数据时(每个元素只会被一个消费者收到)。

使用 Flow
- Flow 代表冷流,是处理异步数据转换、响应式编程以及 UI 状态管理的首选 API
- 需要复杂的操作符链(map、filter、combine等)
- 默认Flow的每个接收者都会接收完整的数据副本,此外Flow可以通过shareIn、stateIn变成SharedFlow、StateFlow热流;
- 网络请求、数据库查询等异步操作
组合使用
kotlin
class SearchViewModel : ViewModel() {
// 使用Channel接收用户输入事件
private val queryChannel = Channel<String>(Channel.CONFLATED)
// 使用 Flow 处理搜索逻辑和 UI 状态
val searchResults: StateFlow<UiState> = queryChannel
.consumeAsFlow()
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { query ->
flow { emit(api.search(query)) }
}
.map { results -> UiState.Success(results) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), UiState.Loading)
fun onQueryChanged(query: String) {
queryChannel.trySend(query)
}
}