协程间数据传递:从Channel到Flow,构建高效的协程通信体系

前言

看到下面的业务场景,阁下改如何面对

Kotlin 复制代码
// 一个下载任务,需要向多个模块实时传递进度
object DownloadManager {
    private var currentProgress = 0
    
    fun startDownload(url: String) {
        // 下载逻辑...
        currentProgress = progress
        // 问题:进度怎么通知给外面?
    }
}

同事的解决方案:写了个静态List存Callback

我问他:为什么不用协程的Channel或Flow?

他说:这两个到底什么区别?什么时候用哪个?

这个问题我见过太多次了。今天我们就来彻底搞懂------协程之间到底怎么高效、安全地传递数据


一、协程数据传递的四个层次

核心认知:

不同的传递场景,对应不同的协程工具。没有银弹,只有最适合。


二、场景1:单次传递 ------ async/await

适用场景: 一个协程等另一个协程的一个结果

Kotlin 复制代码
// ✅ 标准用法:并行请求
viewModelScope.launch {
    val result = coroutineScope {
        val deferred1 = async { api.fetchUser() }
        val deferred2 = async { api.fetchConfig() }
        
        // 等待两个结果
        UserWithConfig(deferred1.await(), deferred2.await())
    }
}

// ✅ 异步转同步
suspend fun fetchData(): Data = coroutineScope {
    val deferred = async(Dispatchers.IO) {
        // 耗时操作
        loadFromDisk()
    }
    deferred.await()  // 挂起直到结果返回
}

本质: async 创建了一个带结果 的协程,await单次消费

不适合: 需要多次发送进度、多个订阅者的场景。


三、场景2:多次传递 ------ Channel

适用场景: 一对一 的生产者-消费者模式,每次数据只能被一个消费者取走

基础用法:生产者-消费者

Kotlin 复制代码
package com.example.cb

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay

/**
 * 生产者
 */
class DataProducer {
    val channel : Channel<String> = Channel()

    suspend fun produceData() {
        repeat(5) {
            val data = "data_$it"
            println("生产:$data")
            channel.send("data_$it")
            delay(200)
        }
        channel.close()
    }
}

/**
 * 消费者
 */
class DataConsumer(private val name : String, private val channel : Channel<String>) {
    suspend fun consumeData() {
        println("consumeData:call ")

        for (data in channel) {
            println("$name 消费 $data")
            delay(10)
        }

        // 另外一种消费方式
//        channel.consumeEach {
//            println("消费:$it")
//        }
    }
}

运行代码

Kotlin 复制代码
val scope = CoroutineScope(Dispatchers.IO)
        val producer = DataProducer()
        val consumer = DataConsumer("张三", producer.channel)

        scope.launch {
            producer.produceData()
        }

        scope.launch {
            consumer.consumeData()
        }

输出结果

生产:data_0

张三 消费 data_0

生产:data_1

张三 消费 data_1

生产:data_2

张三 消费 data_2

生产:data_3

张三 消费 data_3

生产:data_4

张三 消费 data_4

PS: 切记生产者跟消费者必须是不同的协程中,否则只会生产一个数据,但是不会消费,无限被挂起

扇出模式:多个消费者抢数据

生产一个数据后,只有一个会接受到数据。把执行代码修改一下就可以看到:

Kotlin 复制代码
val scope = CoroutineScope(Dispatchers.IO)
val producer = DataProducer()
val consumer0 = DataConsumer("张三", producer.channel)
val consumer1 = DataConsumer("李四", producer.channel)

scope.launch { producer.produceData() }
scope.launch { consumer0.consumeData() }
scope.launch { consumer1.consumeData() }

输出结果

生产:data_0

李四 消费 data_0

生产:data_1

张三 消费 data_1

生产:data_2

李四 消费 data_2

生产:data_3

张三 消费 data_3

生产:data_4

李四 消费 data_4

Channel 的容量选择(面试高频)

前面的是容量都是0, 但是生产速度大于消费速度时,是否要进行缓存。这个缓存的数量时可以设置。 例如设置了10个容量,当消费不到10个时还会继续生产,不会挂起。当大于等于10个时,如果没有出现新的消费者就不会被继续生产。

Kotlin 复制代码
// 🔥 Channel 的四种容量模式
Channel<Int>()                // 默认:RENDEZVOUS,0容量,必须同时等待
Channel<Int>(capacity = 1)    // CONFLATED?不是!这是普通缓冲区
Channel<Int>(Channel.RENDEZVOUS)  // 0容量,发送和接收必须同时
Channel<Int>(Channel.BUFFERED)    // 64容量,系统默认
Channel<Int>(Channel.CONFLATED)   // 1容量,但新数据覆盖旧数据
Channel<Int>(Channel.UNLIMITED)   // 无限制,注意内存!

// CONFLATED 的特殊行为
val conflated = Channel<Int>(Channel.CONFLATED)
launch { 
    conflated.send(1)
    conflated.send(2)  // 1被覆盖
    conflated.send(3)  // 2被覆盖
    conflated.close()
}
launch {
    for (item in conflated) {
        println(item)  // 只输出:3
    }
}

Channel 的核心特性:

  • 一对一消费(扇出时竞争)

  • 背压支持(send挂起,receive挂起)

  • 多订阅者(每个数据只到一个订阅者)

  • 粘性(新订阅者收不到历史数据)

场景3:多对多传递 ------ Flow

flow是什么

Flow 是冷流(Cold Stream)

Kotlin 复制代码
// 定义 Flow
val flow = flow {
    println("Flow 开始执行")
    for (i in 1..3) {
        delay(100)
        emit(i)
    }
}

// ❌ 此时什么都没发生
println("Flow 定义完成")

// ✅ 每次 collect,Flow 代码块重新执行
lifecycleScope.launch {
    flow.collect { println("Collector1: $it") }
} // 输出:Flow 开始执行,1,2,3

lifecycleScope.launch {
    flow.collect { println("Collector2: $it") }
} // 输出:Flow 开始执行,1,2,3(重新执行!)

冷流核心特征:

  • 无消费,不生产 ------没有 collect,Flow 代码块不会执行

  • 每次 collect 独立执行------两个订阅者互不干扰

  • 数据不会被缓存------消费完就丢

SharedFlow/StateFlow 是热流(Hot Stream)

Kotlin 复制代码
// MutableSharedFlow 是热流
val sharedFlow = MutableSharedFlow<Int>()

// 发送数据(不需要有人订阅)
viewModelScope.launch {
    repeat(5) {
        sharedFlow.emit(it)
        delay(100)
    }
}

// 订阅者1:只能收到订阅之后发送的数据
viewModelScope.launch {
    delay(250)  // 延迟订阅
    sharedFlow.collect { println("A: $it") }  // 输出:2,3,4
}

// 订阅者2:也收到同样的数据
viewModelScope.launch {
    sharedFlow.collect { println("B: $it") }  // 输出:0,1,2,3,4
}
维度 Flow SharedFlow/StateFlow
执行时机 每次 collect 执行 启动后常驻内存
订阅者关系 独立执行 共享数据
缓存 可配置 replay
典型场景 数据库查询、网络请求 事件总线、UI状态

基础用法:生产数据发送给所有的订阅者

Kotlin 复制代码
class DataProducer2 {
    private val _datas = MutableSharedFlow<String>()
    val datas = _datas.asSharedFlow()

    suspend fun produceData() {
        repeat(5) {
            val data = "data_$it"
            println("生产:$data")
            _datas.emit(data)
            delay(200)
        }
    }
}

class DataConsumer2(private val name : String, private val datas : SharedFlow<String>) {
    suspend fun consumeData() {
        datas.collect {
            println("$name 消费 $it")
            delay(10)
        }
    }
}

运行代码

Kotlin 复制代码
val scope = CoroutineScope(Dispatchers.IO)
val producer = DataProducer2()
val consumer0 = DataConsumer2("张三", producer.datas)
val consumer1 = DataConsumer2("李四", producer.datas)
scope.launch { producer.produceData() }
scope.launch { consumer0.consumeData() }
scope.launch { consumer1.consumeData() }

结果输出

生产:data_0

张三 消费 data_0

李四 消费 data_0

生产:data_1

张三 消费 data_1

李四 消费 data_1

生产:data_2

张三 消费 data_2

李四 消费 data_2

生产:data_3

张三 消费 data_3

李四 消费 data_3

生产:data_4

张三 消费 data_4

李四 消费 data_4

SharedFlow 的 replay 缓存

Kotlin 复制代码
class StateHolder {
    // replay = 1:新订阅者立即收到最后一个值
    private val _state = MutableSharedFlow<State>(
        replay = 1,  // 🔥 关键配置:缓存最新值
        extraBufferCapacity = 0,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    val state: SharedFlow<State> = _state.asSharedFlow()
    
    suspend fun updateState(newState: State) {
        _state.emit(newState)
    }
}

// 使用
val holder = StateHolder()
holder.updateState(State.Loading)

// 延迟订阅的协程能立即收到 Loading 状态
lifecycleScope.launch {
    holder.state.collect { state ->
        println(state)  // 立即输出:Loading
    }
}

SharedFlow vs Channel 核心区别

Kotlin 复制代码
// 🔥 最直观的区别演示
val channel = Channel<Int>()
val sharedFlow = MutableSharedFlow<Int>()

// 场景:两个订阅者
launch { 
    channel.consumeEach { println("A: $it") }  // 只有一个能收到
}
launch { 
    channel.consumeEach { println("B: $it") }  // 只有一个能收到
}
channel.send(42)  // 要么A收,要么B收,不会都收到

launch { 
    sharedFlow.collect { println("A: $it") }  // 都能收到
}
launch { 
    sharedFlow.collect { println("B: $it") }  // 都能收到
}
sharedFlow.emit(42)  // A和B都收到42

场景4:状态持有 ------ StateFlow

适用场景: 需要最新状态的场景,UI状态管理

StateFlow = SharedFlow(replay=1) + 状态压缩

Kotlin 复制代码
class UserViewModel : ViewModel() {
    // StateFlow 特点:
    // 1. 必须有初始值
    // 2. replay=1 固定
    // 3. 数据相同时不发送(distinctUntilChanged)
    private val _uiState = MutableStateFlow<UiState>(UiState.Idle)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    fun loadUser(id: String) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading  // 直接赋值
            
            try {
                val user = repository.getUser(id)
                _uiState.value = UiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message)
            }
        }
    }
}

// Activity 订阅
lifecycleScope.launch {
    viewModel.uiState
        .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
        .collect { state ->
            when (state) {
                is UiState.Success -> showUser(state.user)
                is UiState.Loading -> showLoading()
                is UiState.Error -> showError(state.message)
                else -> {}
            }
        }
}

5.2 StateFlow 去重特性

复制代码
val state = MutableStateFlow(0)

state.value = 1  // 发送
state.value = 1  // ❌ 值没变,不发送
state.value = 2  // 发送
state.value = 2  // ❌ 不发送

// 如果需要每次都发送,用 SharedFlow

实战对比:进度传递的四种方案

场景:下载任务需要把进度实时传给多个UI组件

❌ 方案1:Channel(错误示范

Kotlin 复制代码
// Channel 不适合一对多广播
class DownloadManager {
    private val progressChannel = Channel<Int>(Channel.CONFLATED)
    
    fun startDownload() {
        repeat(100) {
            progressChannel.send(it)  // ❌ 只有一个订阅者能收到
        }
    }
}

// 进度条A能收到,进度条B收不到

✅ 方案2:SharedFlow(正确方案)

Kotlin 复制代码
class DownloadManager {
    private val _progress = MutableSharedFlow<Int>(
        replay = 0,  // 不需要历史
        extraBufferCapacity = 10,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    val progress: SharedFlow<Int> = _progress.asSharedFlow()
    
    suspend fun download(url: String) {
        repeat(100) {
            delay(50)
            _progress.emit(it)  // ✅ 所有订阅者都收到
        }
    }
}

// 多个UI组件独立订阅
class ProgressBar1 {
    fun bind(manager: DownloadManager) {
        lifecycleScope.launch {
            manager.progress.collect { progress ->
                setProgress(progress)  // 收到进度
            }
        }
    }
}

class ProgressBar2 {
    fun bind(manager: DownloadManager) {
        lifecycleScope.launch {
            manager.progress.collect { progress ->
                setProgress(progress)  // 也收到进度
            }
        }
    }
}

✅ 方案3:StateFlow(如果需显示当前进度)

Kotlin 复制代码
class DownloadManager {
    private val _progress = MutableStateFlow(0)  // 初始值0
    val progress: StateFlow<Int> = _progress.asStateFlow()
    
    fun download(url: String) {
        viewModelScope.launch {
            repeat(100) {
                delay(50)
                _progress.value = it  // 自动去重,但这里值都不同
            }
        }
    }
}

// 新订阅者立即看到当前进度

✅ 方案4:Callback(简单场景)

Kotlin 复制代码
// 如果只在单个页面用,Callback 依然简单有效
fun download(url: String, onProgress: (Int) -> Unit) {
    thread {
        repeat(100) {
            Thread.sleep(50)
            onProgress(it)
        }
    }
}

七、终极选择指南

对比总表

特性 Channel SharedFlow StateFlow async/await
数据次数 多次 多次 多次 单次
订阅者模式 竞争消费 广播 广播 一对一
初始值
粘性 replay控制 ✅ (replay=1)
背压 ✅ send挂起 ✅ emit挂起 ✅ 赋值不挂起
去重
取消 close()
典型场景 任务队列 事件总线 UI状态 并行请求

一句话选型

- 要一个结果:async/await
- 要任务队列:Channel
- 要事件广播:SharedFlow
- 要UI状态:StateFlow
- 不会用协程的人:Callback

面试高频题

Q1:SharedFlow 和 StateFlow 的区别?

答:

  1. StateFlow 必须有初始值,SharedFlow 不需要

  2. StateFlow 自动去重,SharedFlow 发送全部数据

  3. StateFlow 继承自 SharedFlow,replay=1 固定

  4. StateFlow 通过 value 读写,SharedFlow 通过 emit/collect

Q2:Channel 和 Flow 怎么选?

答:

  • Channel 是热流,生产者和消费者直接耦合

  • Flow 是冷流,每次 collect 重新执行

  • 需要背压 用 Channel,需要多订阅者用 SharedFlow

Q3:SharedFlow 缓冲区满了怎么办?

答: 三种策略:

  • SUSPEND:挂起发送方

  • DROP_OLDEST:丢弃最旧数据

  • DROP_LATEST:丢弃最新数据


写在最后

协程间数据传递,本质是并发模型的选择。

不懂 Channel,不知道怎么写生产者-消费者;
不懂 SharedFlow,不知道怎么做事件总线;
不懂 StateFlow,不知道怎么做状态管理。

但最可怕的不是不懂,而是用错了工具------

  • 用 Channel 做广播 → 丢数据

  • 用 StateFlow 做事件 → 丢事件

  • 用 async 做多次传递 → 只能等一次

相关推荐
ShoreKiten1 小时前
Upload-labs 高版本php环境非完全攻略
开发语言·php
前端不太难1 小时前
为什么鸿蒙不再适用 Android 分层
android·状态模式·harmonyos
哈库纳1 小时前
dbVisitor 利用 queryForPairs 让键值查询一步到位
java·后端·架构
2501_916007471 小时前
ios上架 App 流程,证书生成、从描述文件创建、打包、安装验证到上传
android·ios·小程序·https·uni-app·iphone·webview
hoiii1872 小时前
拉丁超立方抽样(LHS)的MATLAB实现:基本采样与相关采样
开发语言·算法
~央千澈~2 小时前
抖音弹幕游戏开发之第6集:解析JSON数据·优雅草云桧·卓伊凡
开发语言·python·php
郝学胜-神的一滴2 小时前
深入解析Python中dict与set的实现原理
开发语言·python
野犬寒鸦2 小时前
CompletableFuture 在 项目实战 中 创建异步任务 的核心优势及使用场景
java·服务器·后端·性能优化
Java小卷2 小时前
Drools kmodule 与 ruleunit 模块用法详解
java·后端