一、概念
1.1 通信模式
根据接收端节点数量以及接收方式,可分为三类。

| 单播 Unicast | 一对一,对单个订阅者发送。 | |
| 组播 Multicast | 分配性:一对多,依次对单个订阅者发送。多个订阅者互斥,收到的值不是同一个。 | 同步性:元素只能被一个接收端消费。 |
| 广播 BroadCast | 共享性:一对多,同时对全体订阅者发送。多个订阅者共享,接收到的值是同一个。 | 并发性:一次发送多处消费。 |
1.2 扇入扇出
| 扇入 Fan-in | 指直接调用该模块的上级模块的个数(多个发送端),即多个协程可能会向同一个Channel发送值。扇入大表示模块的复用程序高。 |
| 扇出 Fan-out | 指该模块直接调用的下级模块的个数(多个接收端),即多个协程可能会从同一个Channel中接收值。扇出大表示模块的复杂度高。 |
二、协程间通信
出现在Flow之前,现在退居幕后职责单一,仅作为协程间通信的并发安全的缓冲队列而存在。
SendChannel在创建时定义了消费方式外界只能往里发送值,ReceiveChannel在创建时定义了生产方式外界只能从中获取值,Channel继承了它俩既能发送也能接收,根据实际需求暴露不同类型收窄功能。
- 非阻塞:类似于 Java 中的 BlockingQueue 队列,不同的是 put() 和 take() 读取写入数据是阻塞的,而 Channel 中的 send() 和 receive() 是挂起的。
- 同步性:每个值只能被众多订阅者中的一个消费。
- 并发安全:没有检测到 receive() 的话 send() 就会挂起不会发送值(默认模式 RENDEZVOUS,没有缓冲区的Channel是同步的)。
- 公平性:在多个协程中发送或接收(多线程竞争)遵循先进先出(FIFO即队列)。
|
SendChannel 生产者通道 | send() |
public suspend fun send(element: E) 将元素发送到通道,缓冲区已满时将被挂起,直到有旧元素被消费腾出空间。 |
| trySend() |
public fun trySend(element: E): ChannelResult<Unit> 如果不违反其容量限制,则立即将指定元素添加到此通道,并返回成功结果。否则返回失败或关闭的结果。 | |
| close() |
public fun close(cause: Throwable? = null): Boolean 调用后表示关闭发送功能,此时 isClosedForSend() 会返回true,继续发送元素会报错ClosedSendChannelException。缓冲区里的元素可以继续被消费,等所有元素被消费后 isClosedForReceive() 会返回true,for循环会自动结束,继续消费会报错ClosedReceiveChannelException。具有原子性。 | |
| isClosedForSend() |
public val isClosedForSend: Boolean 判断通道是否关闭了发送功能,若为true,调用 send() 继续发送元素会报错ClosedSendChannelException。 | |
|
ReceiveChannel 消费者通道 | receive() |
public suspend fun receive(): E 从通道中消费一个元素并移除,通道中没有元素时将被挂起,直到有新元素被发送进来。 |
| tryReceive() |
public fun tryReceive(): ChannelResult<E> 当通道中有值时就从中消费,并返回成功结果。否则返回失败或关闭的结果。 | |
| receiveCatching() |
public suspend fun receiveCatching(): ChannelResult<E> 如果此通道不为空,则从中检索并删除元素,返回成功结果;如果通道为空,则返回失败结果;如果通道关闭,则返回关闭的原因。 | |
| isEmpty() |
public val isEmpty: Boolean 若通道中没有元素且接收未被关闭则返回true。 | |
| isClosedForReceive() |
public val isClosedForReceive: Boolean 判断通道是否关闭了消费功能,若为ttrue,调用 receive() 继续消费元素会报错ClosedReceiveChannelException。 | |
| cancel() |
public fun cancel(cause: CancellationException? = null) 以可选原因直接关闭通道,缓存中未消费的元素会被丢弃,可在通过构造方式创建通道时指定 onUndeliveredElement() 进行处理。 | |
| iterator() |
public operator fun iterator(): ChannelIterator<E> 返回通道的迭代器。 |
- send() 和 receive():挂起函数。当接收时,Channel中没有元素,协程将被挂起直到元素可用。当发送时,Channel容量达到阈值,协程将被挂起直到容量可用。
- trySend() 和 tryReceive():普通函数。从非挂起函数中发送或接收元素,操作是即时的并返回 ChannelResult 对象,包含了有关操作成功或失败的信息。
- close() 和 cancel():当创建的是 Channel 类型的时候,两个都可以用来关闭,close() 会消费完缓冲区中的元素,cancel() 会直接关闭并丢未消费的元素。通道使用完一定要关闭,否则代码块不会停止,造成协程阻塞和内存泄漏。
fun main() = runBlocking {
private val _channel = Channel<String>()
val receiveChannel: ReceiveChannel<String> = _channel
val sendChannel: SendChannel<String> = _channel
_channel.send("Channel发送")
sendChannel.send("SendChannel发送")
delay(1000)
_channel.consumeEach { println("Channel接收:$it") }
receiveChannel.consumeEach { println("ReceiveChannel接收:$it") }
}
2.1 创建
produce() 和 actor() 被定义成协程构建器(因此只能在协程环境中调用,会在异常、完成、取消时自动关闭),同 launch、async 一样作为 CoroutineScope 的扩展函数。
2.1.1 actor() 创建 SendChannel
创建时在协程构建器 actor() 的 Lambda 中定义了数据的消费方式,返回一个生产者通道 SendChannel,其它协程通过该对象往里发送数据。
| public fun <E> CoroutineScope.actor( context: CoroutineContext = EmptyCoroutineContext, capacity: Int = 0, start: CoroutineStart = CoroutineStart.DEFAULT, onCompletion: CompletionHandler? = null, block: suspend ActorScope<E>.() -> Unit ): SendChannel<E> |
fun main() = runBlocking {
//创建生产者通道
val send: SendChannel<Int> = actor {
for (i in channel) println(i) //定义了消费方式
}
//其它协程拿来发送数据
launch {
(1..3).forEach { sendChannel.send(it) }
}
}
2.1.2 produce() 创建 ReceiveChannel
创建时在协程构建器 produce() 的 Lambda 中定义了数据的生产方式,返回一个消费者通道 ReceiveChannel,其它协程通过该对象从中取出数据。
|
public fun <E> CoroutineScope.produce( |
fun main() = runBlocking {
//创建消费者通道
val receiveChannel: ReceiveChannel<Int> = produce {
(1..3).forEach { send(it) } //定义了生产方式
}
//其它协程拿来接收数据
launch {
for (i in receiveChannel) println(i)
}
}
2.1.3 构造创建 Channel
| public fun <E> Channel( capacity: Int = RENDEZVOUS, //容量 onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, //容量溢出后策略 onUndeliveredElement: ((E) -> Unit)? = null //异常回调 ): Channel<E> |
|
capacity 缓冲区容量 |
没有缓冲区的Channel是同步的。当缓冲区为空时(缓存中没有元素),receive会被挂起。 RENDEZVOUS(默认):或指定为0。缓冲区大小为0且为SUSPEND策略,每次一个值,receive和send同步交替,另一方没准备好自己就会挂起。(并发安全) CONFLATED:或指定为1。只有1个值,新值覆盖旧值,send永不挂起。此时溢出策略只能设为SUSPEND。 BUFFERED:或指定为-2(默认为64个值)或指为>1的具体值。缓冲区满了后根据溢出策略决定send是否被挂起(挂起直到消费后腾出空间)。 UNLIMITED:无限容量(Int.MAX_VALUE)。send永不挂起,能一直往里发送数据。实际开发一般不用,容易内存溢出。 |
|
onBufferOverflow 缓冲区满后溢出策略 |
当容量 >= 0 或容量 == Channel.BUFFERED 时才会触发。 BufferOverflow.SUSPEND满了后send会挂起。 BufferOverflow.DROP_OLDEST丢弃最旧的值,发送新值。 BufferOverflow.DROP_LATEST丢弃新值。 |
|
onUndeliveredElement 异常回调 | 元素没有被消费时回调,丢弃的元素会在这里收到。通常用来关闭由Channel发送的资源(值)。 |
val rendezvousChannel = Channel<String>() //约会类型
val bufferedChannel = Channel<String>(10) //指定缓存大小类型
val conflatedChannel = Channel<String>(Channel.CONFLATED) //混合类型
val unlimitedChannel = Channel<String>(Channel.UNLIMITED) //无限缓存大小类型
fun main() = runBlocking {
val channel = Channel<Int>(capacity = Channel.CONFLATED) {
println("onUndeliveredElement: $it")
}
launch {
(1..3).forEach {
println("send: $it")
channel.send(it)
}
channel.close() //不要忘记关闭
}
launch {
for (i in channel) {
println("receive: $i")
}
}
println("结束!")
}
打印:
结束!
send: 1
send: 2
onUndeliveredElement: 1
send: 3
onUndeliveredElement: 2
receive: 3
2.2 遍历元素
2.2.1 Iterate 迭代器
fun main(): Unit = runBlocking {
val channel = Channel<String>(Channel.UNLIMITED)
repeat(5) { channel.send("$it") }
val iterator = channel.iterator()
while (iterator.hasNext()) {
println("【iterator】${iterator.next()}")
delay(1000)
}
//5个元素打印完后程序没结束,Channel不会关闭,后面代码执行不到
println("这行执行不到")
}
2.2.2 for 循环
fun main():Unit = runBlocking {
val channel = Channel<String>(Channel.UNLIMITED)
repeat(5) { channel.send("$it") }
for (i in channel) {
println("【for】$i,")
delay(1000)
}
//5个元素打印完后程序没结束,Channel不会关闭,后面代码执行不到
println("这行执行不到")
}
2.2.3 扩展函数
会在代码块执行完后确保关闭通道,但无法保证在代码块执行完后不会有新元素进入,新元素会被丢弃,可在通过构造方式创建通道时指定 onUndeliveredElement() 进行处理。
| consume() |
public inline fun <E, R> ReceiveChannel<E>.consume(block: ReceiveChannel<E>.() -> R): R 执行完后关闭通道。 |
| consumeEach() |
public suspend inline fun <E> ReceiveChannel<E>.consumeEach(action: (E) -> Unit): Unit 将 action 应用于每一个元素,执行完后会关闭通道。 |
//只消费第一个元素并关闭通道
suspend fun <E> ReceiveChannel<E>.consumeFirst(): E = consume { return receive() }
2.2.4 转换成 Flow
底层通过 ChannelFlow 实现。Channel 转成 Flow 是为了使用那些方便的操作符,同时 Flow 的很多功能扩展底层由 Channel 实现(如flowOn、buffer)。
| consumeAsFlow() |
public fun <T> ReceiveChannel<T>.consumeAsFlow(): Flow<T> = ChannelAsFlow(this, consume = true) 只能被收集一次(只能有一个消费者),多次收集抛异常 IllegalStateException。 |
| receiveAsFlow() |
public fun <T> ReceiveChannel<T>.receiveAsFlow(): Flow<T> = ChannelAsFlow(this, consume = false) 采用扇出模式(可以有多个消费者),一个元素只能被消费一次,该元素的消费者不确定是哪个。 |
三、广播
BroadcastChannel 在协程1.4版本已被废弃,取而代之的是 SharedFlow(StateFlow是它的特定配置版本)。
3.1 SharedFlow
SharedFlow只能订阅(消费),FlowCollector只能发送(生产),MutableSharedFlow继承了它俩既能发送也能订阅,根据实际需求暴露不同类型收窄功能。接收会一直监听,通过取消协程来关闭它的收集。
| SharedFlow |
public interface SharedFlow<out T> : Flow<T> { //缓存的回放元素的快照 //收集元素 override suspend fun collect(collector: FlowCollector<T>): Nothing |
| FlowCollector | public fun interface FlowCollector<in T> { public suspend fun emit(value: T) //发送元素 } |
| MutableSharedFlow | public interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> { // 发射元素(注意这是个挂起函数) override suspend fun emit(value: T) // 发射元素(注意这是个普通函数,如果缓存溢出策略是 SUSPEND,溢出时就不会挂起了而是直接返回 false) public fun tryEmit(value: T): Boolean // 活跃订阅者数量,将它设为0生产元素就会停止用来释放资源。 public val subscriptionCount: StateFlow<Int> //清空当前回放里的历史记录 public fun resetReplayCache() } |
fun main() = runBlocking {
//利用多态暴露不同父类限制功能给外部使用
private val _mutableSharedFlow = MutableSharedFlow<String>()
val sharedFlow: SharedFlow<String> = _mutableSharedFlow //或调用 asSharedFlow()
val flowCollector: FlowCollector<String> = _mutableSharedFlow
launch { _mutableSharedFlow.collect { println("mutableSharedFlow接收:$it") } }
launch { sharedFlow.collect { println("mutableSharedFlow接收:$it") } }
delay(1000)
mutableSharedFlow.emit("mutableSharedFlow发送")
flowCollector.emit("flowCollector发送")
}
3.1.1 通过构造创建
|
public fun <T> MutableSharedFlow( |
|
onBufferOverflow 缓存溢出策略 |
1.只有存在订阅者时才会触发缓存溢出策略,否则直接丢弃。(例如两个launch虽然会并行执行,若下方的消费launch在上方的生产launch后执行,先发送的值都会被丢弃接收不到,直到消费launch执行了才开始接收后面的值。这是和 Channel 最大的区别,没有消费者 Channel 不会丢) 2.只有在回放或缓存容量>0时,才支持两种丢弃模式。 |
|
BufferOverflow.SUSPEND:挂起。 BufferOverflow.DROP_OLDEST:丢弃最旧的。 BufferOverflow.DROP_LATEST:丢弃最新的。 |
suspend fun method(): Unit = coroutineScope { //让3个launch同时进行
val shared = MutableSharedFlow<Int>(3) //回放3个数据
launch {
for(i in 1..5){
shared.emit(i)
println("emmit:$i")
//如果这里不延迟,虽然launch是并行执行,发送是很快的这里才5个值
//相当于是没有订阅者的,5个值都会被丢弃
//除非把下面的订阅launch写在这个launch上面
delay(1000) //1秒更新一个值
}
}
//订阅者甲
launch { shared.collect{ println("甲:$it") } }
//订阅者乙5秒后再订阅,也就是值全都更新完了再订阅
delay(5000)
launch { shared.collect{ println("乙:$it") } }
}
打印:
emmit:1
甲:1
emmit:2
甲:2
emmit:3
甲:3
emmit:4
甲:4
emmit:5
甲:5
乙:3 //回放了3个已更新过的数据
乙:4
乙:5
3.1.2 转换 Flow → SharedFlow
| public fun <T> Flow<T>.shareIn( scope: CoroutineScope, //数据共享时所在的协程作用域 started: SharingStarted, //启动策略 replay: Int = 0 //回放,新订阅时得到几个之前已经发射过的旧值。 ): SharedFlow<T> |
|
started 启动策略 | SharingStarted.Eagerly | 立即发送数据(直到scope结束)。 |
| SharingStarted.Lazily | 在首个订阅者观察时才开始发送数据(当订阅者都没了还是活跃的,直到scope结束)。这保证了第一个订阅者能获得所有值,后续订阅者获得最新replay数量的值。 | |
| SharingStarted.WhileSubscribed |
在首个订阅者观察时才开始发送数据,直到最后一个订阅者消失时停止,当又有新订阅者时会再次启动,避免引起资源浪费(例如一直从数据库、传感器中读取数据)。提供了两个配置:
|
3.2 StateFlow
StateFlow继承自SharedFlow是一种特殊配置,相当于MutableSharedFlow(1,0, BufferOverflow.DROP_OLDEST),可以使用value属性来访问值,可以当作是用来取代LiveData。
- 必须传入默认值:Null安全。
- 回放个数1+额外缓冲区大小0:只持有1个值。
- 缓存策略DROP_OLDEST:只持有最新值(新值会替换旧值)。
- 数据防抖:即仅在新值内容发生变化才会消费。
| StateFlow | public interface StateFlow<out T> : SharedFlow<T> { // 当前值 public val value: T } |
| MutableStateFlow | public interface MutableStateFlow<T> : StateFlow<T>, MutableSharedFlow<T> { // 当前值 public override var value: T // 比较并设置(通过 equals 对比,如果值发生真实变化返回 true) public fun compareAndSet(expect: T, update: T): Boolean } |
3.2.1 通过构造创建
|
public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL) 形参value是默认值。 |
val state = MutableStateFlow(0) //默认值会被覆盖
launch {
for(i in 1..5){
state.emit(i)
println("emit:$i")
delay(1000) //1秒更新一个值
}
}
launch {
delay(2000) //2秒后开始订阅
state.collect{ println("collect:$it") }
}
打印:
emit:1
emit:2
collect:2 //2秒后开始订阅,不会收到之前已更新的数据
emit:3
collect:3
emit:4
collect:4
emit:5
collect:5
3.2.2 转换 Flow → StateFlow
|
public fun <T> Flow<T>.stateIn( |
|
public suspend fun <T> Flow<T>.stateIn(scope: CoroutineScope): StateFlow<T> { 挂起函数版本,不用指定默认值,会挂起直到产出第一个值。 |
四、区别及选择
4.1 通信(Channel) or 广播(SharedFlow)

| Channel | SharedFlow |
| 必须性:事件不会被丢弃且必须执行。 | 时效性:过期的事件没有意义且不应该被延迟消费(就像广播电台,不管有没有人收听都在播放内容,当你开始收听的时候只能听到后续新内容,之前的内容就是错过)。 |
| 分配性:挨个对订阅者发送,多个订阅者互斥,收到的值不是同一个。 | 共享性:同时对全体订阅者发送,多个订阅者共享,接收到的值是同一个。 |
| 同步性:事件只能消费一次。 | 并发性:事件会被多处消费。 |
4.2 事件(Event) or 状态(State)
事件按顺序都要执行到,状态只关心最新值。
| SharedFlow | StateFlow | |
| 类型 | 回放和额外缓存默认为0,无订阅者直接丢弃数据,符合时效性事件特点。 | 仅持有单个且最新的数据。 |
| 初始值 | 无(事件发生后才处理,不需要默认值) | 有(UI组件应当一直有一个值来表明其状态) |
| 回放 |
默认0可配置(新的订阅者不重复处理已发生过的事情,或者需要知道之前发生过的事件) |
1(新的订阅者也应该知道当前状态,若用作事件处理会出现粘性事件) |
| 额外缓冲区 | 默认0可配置 |
0(UI组件只显示最新的值) |
| 缓存模式 | 默认SUSPEND(等待消费) | DROP_OLDEST(UI组件只显示最新的值) |
| 发送重复的值 | 会消费(事件都应该被处理)。 |
不消费(防抖,无变化不用处理)。 |
| 收集方式 | 只能调用方法 collect() 在协程中收集。 | 既可以通过调用属性 value 随处收集,也可以调用方法 collect() 在协程中收集。 |
本文介绍Kotlin协程中的热流,包括Channel、SharedFlow和StateFlow。Channel作为协程间通信的并发安全缓冲队列,有多种创建和操作方式。SharedFlow和StateFlow用于广播,StateFlow是SharedFlow的特定配置版本。还阐述了组播与广播、事件与状态的区别及选择。
626

被折叠的 条评论
为什么被折叠?



