Android | Channel 与 Flow的异同点

0 阅读5分钟

Channel 和 Flow 都是 Kotlin 协程中处理异步数据流的工具,但它们的设计理念和使用场景有很大不同。

对比

特性ChannelFlow
数据发射热数据流冷数据流
多消费者共享(每个元素只被一个消费者接收)独立(每个消费者重新发射数据)
背压处理通过缓冲区策略通过操作符(buffer、conflate等)
状态有状态,独立于消费者存在无状态,依赖消费者存在
使用场景事件总线、任务队列、通信数据转换、响应式编程、UI状态

解释与示例

1. 热数据流 vs 冷数据流

热流、冷流的主要区别在于数据流的生产与消费之间的关系:

  • 热流 :即使没有收集者(Collector)在监听,数据生产者也会立即开始生成数据。数据是共享的,新的收集者加入时只会收到其订阅之后发出的数据(除非设置了重放机制如StateFlow 的初始值)。Channel 就像一个管道,数据被推送到管道中,等待被接收者获取。
  • 冷流数据生产者只在有收集者开始收集时才执行生产逻辑。每次有新的收集者,都会从头开始重新生成一份完整的数据流,实现一对一的按需生产。常规的 Flow 就是典型的冷流,当然也可以转换成SharedFlow,、StateFlow热数据流。

总结来说:

特性冷流 (Flow)热流 (Channel, SharedFlow, StateFlow)
生产时机有收集者时才生产独立于收集者,立即生产
数据共享不共享,一对一共享,一对多
生命周期绑定到收集协程的生命周期独立存在,需要手动取消或绑定到外部作用域

Channel(热数据流)

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")
        }
    }
}

执行结果:

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,发送不会被挂起,如果上述代码中改成:

val channel = Channel<Int>(Channel.RENDEZVOUS) //或者不传,默认就是RENDEZVOUS
//...其他不变...

设置RENDEZVOUS没有缓冲区,当没有接收时会挂起,所以发送跟接收是交替执行的,执行结果:

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(冷数据流)

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")
        }
    }
}

执行结果:

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 元素竞争,存在多个接收者时可能会交替接收数据,示例:

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)
            }
        }
    }
}

执行结果:

16:21:45.948  E  消费者0 收到: A
16:21:45.949  E  消费者1 收到: B
16:21:46.049  E  消费者0 收到: C

可以看到多个接收者时可能会交替接收发送者的数据。

Flow 独立数据流

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")
            }
        }
    }
}

执行结果:

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 背压

// 通过缓冲区策略处理背压
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 背压

activity.lifecycleScope.launch {
    flow {
        for (i in 1..10) {
            emit(i)
        }
    }
        .buffer(10) // 添加缓冲区
        .collect { value ->
            delay(500) // 慢消费者
            log("接收 $value")
        }
}

执行结果:

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(),则只会处理最新值,执行结果变成:

17:16:45.248  E  接收 1
17:16:45.749  E  接收 20

4. 互相转换

Channel 转 Flow

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") } // 可能收不到数据
    }
}

执行结果:

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

val flow = flow { /* ... */ }
//将冷流转为热流
val channel: ReceiveChannel = flow.produceIn(CoroutineScope(Dispatchers.Default))

不过该转换基本不用了,Flow已经被设计为处理异步数据流的主要API。所以绝大多数场景下,Flow的声明性、结构化并发和丰富的操作符更适合。

如何选择

使用 Channel

  • Channel 代表 热流 的底层实现,类似于一个阻塞队列(Blocking Queue)或管道。数据发送者独立于接收者运行。
  • 底层并发:需要一个底层的、线程安全的队列来进行协程间的通信。示例:Kotlin协程并发控制:多线程环境下的顺序执行
  • 多个发送者/多个接收者: 当需要多个独立的协程向同一个管道发送数据,并且多个协程从同一个管道竞争性地接收数据时(每个元素只会被一个消费者收到)。

channel

使用 Flow

  • Flow 代表冷流,是处理异步数据转换、响应式编程以及 UI 状态管理的首选 API
  • 需要复杂的操作符链(map、filter、combine等)
  • 默认Flow的每个接收者都会接收完整的数据副本,此外Flow可以通过shareIn、stateIn变成SharedFlow、StateFlow热流;
  • 网络请求、数据库查询等异步操作

组合使用

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)
    }
}