Android | Channel 与 Flow的异同点

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)
    }
}
相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui