前言
我们先来介绍一下几个关键知识点:
-
StateFlow:状态流,它是为 UI 层提供状态订阅的。每当状态的值发生变化,都会通知给订阅者。新订阅者总能收到最新的状态。
-
SharedFlow:事件流,它是StateFlow的底层实现,提供事件订阅。默认情况下,订阅前发生的事件不会推送给新订阅者。
-
Flow:冷数据流,它是SharedFlow的底层,只有在被收集 (collect) 时才会执行。 -
Channel:协程间协作工具,它是Flow的关键底层支撑,用于协程间传递数据。它的功能和
async类似,只不过它是多条数据的async。并且async是一次性的,而Channel能够多次发送数据。
我们从 Channel 开始。
Channel 是什么?
要理解它,我们先要来看看 async 和 await。
我们可以使用 async 启动一个并发协程,并在其他协程中调用 await 获取到它的结果,它通过 API 的拆分,将异步协程的启动和结果的获取分开了。
kotlin
fun main() = runBlocking<Unit> {
val deferredFirst: Deferred<String> = async(Dispatchers.IO) {
delay(5000) // 模拟耗时操作
"Result-First"
}
val deferredSecond: Deferred<String> = async(Dispatchers.IO) {
delay(3000) // 模拟耗时操作
"Result-Second"
}
launch {
// 模拟具有实际业务功能的挂起函数
delay(2000)
val result = deferredFirst.await() + "and" + deferredSecond.await()
println("the result is $result")
}
}
如果我们想要多次获取结果,需要一个持续的数据流,就不能使用 async 了,因为它只能返回一次,并且多次调用的结果都相同。
这时,可以使用 Channel,你可以把它看作是可以多次返回结果的 async。
kotlin
fun main() = runBlocking<Unit> {
// produce 启动一个协程,并返回一个 ReceiveChannel
val receiveChannel: ReceiveChannel<String> = produce(Dispatchers.Default) {
while (isActive) {
val currentTimeString =
DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US)
.format(
Date(System.currentTimeMillis())
)
send(currentTimeString) // 使用 send 多次生产(发送)数据
delay(1000)
}
}
// 在另一个协程中消费数据
launch {
while (isActive) {
// 获取新数据
val time = receiveChannel.receive()
println("Current Time is: $time")
delay(1000)
}
}
// 让程序运行 5 秒钟后停止
delay(5000)
coroutineContext.cancelChildren() // 取消 produce 和 launch
}
CoroutineScope.produce 也是协程构建器,它所启动的生产者协程,是用来给其他协程提供数据的。在不生产数据时,可当作普通的 launch 使用。
它内部提供了 ProducerScope,这个作用域同时实现了 CoroutineScope 和 SendChannel:
kotlin
public interface ProducerScope<in E> : CoroutineScope, SendChannel<E> {
public val channel: SendChannel<E>
}
它会返回一个 ReceiveChannel(类似 async 的返回的 Deferred),可调用它的 receive() 挂起等待数据(类似 Deferred.await())。
运行结果:
less
Current Time is: Oct 24, 2025, 1:49:58 PM
Current Time is: Oct 24, 2025, 1:49:59 PM
Current Time is: Oct 24, 2025, 1:50:00 PM
Current Time is: Oct 24, 2025, 1:50:01 PM
Current Time is: Oct 24, 2025, 1:50:02 PM
Channel 的本质:挂起式队列
ProducerScope 作用域对象和返回的 ReceiveChannel 对象其实是同一个对象。
进到 CoroutineScope.produce() 函数内部创建的 ProducerCoroutine 对象中,可以看到:这个对象继承了 ChannelCoroutine 类,ChannelCoroutine 又实现了 Channel 接口。
而 Channel 接口同时实现了 SendChannel 和 ReceiveChannel 接口:
kotlin
public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> {
// ..
}
Channel 接口同时具备发送数据和接收数据的功能,这说明了在 produce() 函数发送数据和在其他协程中获取数据时,操作的是同一个对象。
produce 实际上是对 Channel 模型的一种封装,我们来看看 Channel 最纯粹、最核心的用法:
kotlin
fun main() = runBlocking<Unit> {
// 创建 Channel 对象
val channel = Channel<String>()
// 发送数据
launch {
delay(1000)
channel.send("Hello, ")
delay(3000)
channel.send("Channel!")
}
// 接收数据
launch {
print(channel.receive())
print(channel.receive())
}
}
可以看出,Channel 本质上就是挂起队列,类似 Java 中的 BlockingQueue(阻塞队列)。
不过,当 Channel 队列满了之后,再往队列中添加元素(send())会挂起当前协程,而不是阻塞线程,直到队列有空位;当队列为空时,尝试从队列中获取元素(receive())会挂起当前协程,而不是阻塞线程,直到队列有元素插入。
知道了它的队列本质,我们也就知道了 Channel 并不适合做可订阅的事件流。它的一个元素最多只能被一个消费者接收,当订阅者超过一个时,会导致每个订阅者都无法接收到完整的事件序列:
kotlin
fun main() = runBlocking<Unit> {
// 创建 Channel 对象
val channel = Channel<String>()
launch(Dispatchers.IO) {
channel.send("event-1")
channel.send("event-2")
channel.send("event-3")
}
launch {
for (data in channel) { // receive() 的简便写法,遍历时会将当前协程挂起
println("A received: $data")
delay(100)
}
}
launch {
for (data in channel) { // 同样是挂起式的遍历
println("B received: $data")
delay(100)
}
}
}
运行结果:
less
A received: event-1
B received: event-2
A received: event-3
在开发中,我们会使用
SharedFlow来实现事件流的订阅。
那么,Channel 有什么用?
首先它作为 Flow 的底层支持,很多 Flow 的操作符就是使用的 Channel。其次,在模块内部,能确保只有一个订阅者的时候,我们就可以使用它,因为它简单高效。
容量、策略与生命周期
容量 (Capacity)
默认情况下,Channel 队列的长度是 0。创建 Channel 使用的容量值为 RENDEZVOUS(= 0),它的意思是"会合",也就是 send()、receive() 会合时,才会交接数据,完全没有缓冲。
这意味着发送数据时会一直挂起,直到其他协程接收数据;其他协程接收数据时也会一直挂起,直到当前协程发送数据。
我们可以给 Channel 指定容量:
kotlin
// 缓冲区容量为 10
val channel = Channel<Int>(capacity = 10)
此时,调用 send() 会立即返回(将数据放入缓冲区)。直到缓冲区满了后,之后调用的 send() 才会挂起。
我们也可以使用 Channel 提供的容量值,UNLIMITED 和 BUFFERED。
UNLIMITED 的值为 Int.MAX_VALUE,这个值应该慎重选择;而 BUFFERED 的容量默认是 64。
kotlin
// Channel.kt
internal val CHANNEL_DEFAULT_CAPACITY = systemProp(DEFAULT_BUFFER_PROPERTY_NAME,
64, 1, UNLIMITED - 1
)
缓冲溢出策略
当缓冲区满了之后,再次发送数据默认会挂起 ,这就是缓冲溢出。当然这也是背压的体现:下游处理速度跟不上上游发送速度。
我们可以改变这个策略:
kotlin
// 默认挂起当前协程
val channel1 = Channel<String>(capacity = 10, onBufferOverflow = BufferOverflow.SUSPEND)
// 丢弃最旧的数据,将新数据放进队尾
val channel2 = Channel<String>(capacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST)
// 丢弃当前要发送的新数据
val channel3 = Channel<String>(capacity = 10, BufferOverflow.DROP_LATEST)
CONFLATED
其实,Channel 还提供了另一种预设的容量值:CONFLATED。
它是融合、合并的意思,它的容量只有 1,同时缓冲溢出的策略是 DROP_OLDEST。
这意味着它只会关心最新的数据,生产者会不断使用新数据替换缓冲区未被消费的旧数据,消费者永远会拿到最新数据。
这正是
Flow中的conflate()操作符的实现原理。
注意:当使用 CONFLATED 时,缓冲策略必须不填,或者为默认值 SUSPEND。
Channel 的关闭
Channel 的关闭有两个函数,分别是 SendChannel.close() 和 ReceiveChannel.cancel()。前者由生产者调用,表示没有更多数据需要发送了;后者由消费者调用,表示不再需要数据了。
close()
在 SendChannel 接口的内部有一个 isClosedForSend 属性,表示是否已经关闭了发送功能。这个属性的默认值为 false,当我们调用了 close() 函数后,它会变为 true。
当这个标志为 true 时,禁止再次发送数据(调用 send() 会抛出 ClosedSendChannelException),但我们还是可以调用 receive() 来接收数据,直到缓冲区为空。
当缓冲区为空后,ReceiveChannel.isClosedForReceive 会变为 true,此时,再调用 receive() 来接收数据会抛出 ClosedReceiveChannelException。最后,for 循环会自动正常结束。
kotlin
fun main() = runBlocking<Unit> {
val channel = Channel<String>(capacity = 2)
// 生产者
launch(Dispatchers.Default) {
channel.send("event-1")
channel.send("event-2")
channel.send("event-3")
channel.close()
}
// 消费者
launch(Dispatchers.Default) {
delay(1000)
try {
for (data in channel) {
println("A received: $data")
delay(1000)
}
} catch (e: Exception) {
// 不应该捕获到异常
println("A finished with exception: $e")
}
println("A: isClosedForReceive = ${channel.isClosedForReceive}")
// 尝试再次接收
try {
val data = channel.receive()
println("A received after close: $data")
} catch (e: Exception) {
println("A after close with exception: $e")
}
println("A finished")
}
}
运行结果:
less
A received: event-1
A received: event-2
A received: event-3
A: isClosedForReceive = true
A after close with exception: kotlinx.coroutines.channels.ClosedReceiveChannelException: Channel was closed
A finished
cancel()
当我们调用 ReceiveChannel.cancel() 时,它会关闭双端,也就是将 isClosedForReceive 和 isClosedForSend 标志都设为 true。
当发送或接收数据时,会抛出 CancellationException 异常,并且缓冲区那些还未被消费的数据会被丢弃。
kotlin
fun main() = runBlocking<Unit> {
val channel = Channel<String>(capacity = 10)
// 生产者
launch(Dispatchers.Default) {
try {
channel.send("event-1")
channel.send("event-2")
delay(2000)
channel.send("event-3") // 永远不会被发送
} catch (e: Exception) {
// 这里会捕捉到 CancellationException
println("Producer caught exception: $e")
}
}
// 消费者
launch(Dispatchers.Default) {
try {
println("A received: ${channel.receive()}")
println("A received: ${channel.receive()}")
channel.cancel()
for (data in channel) {
println("A received in loop: $data")
}
} catch (e: Exception) {
// 这里会捕捉到 CancellationException
println("A finished with exception: $e")
}
println("A finished")
}
}
运行结果:
less
A received: event-1
A received: event-2
A finished with exception: java.util.concurrent.CancellationException: Channel was cancelled
A finished
Producer caught exception: java.util.concurrent.CancellationException: Channel was cancelled
资源清理
如果在 Channel 中传递的数据是文件句柄或是数据库连接等需要关闭的资源,直接丢弃(包括因缓冲策略被丢弃的数据)可能会导致资源泄露。
这时,我们可以在创建 Channel 传入 onUndeliveredElement 回调,从而安全地关闭资源:
kotlin
val channel = Channel<FileWriter>(
capacity = 3,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
onUndeliveredElement = { fileWriter ->
fileWriter.close()
})
actor 模式
produce 封装了 Channel 的创建和发送,而 actor 函数则是封装了创建和接收的逻辑。
它内部提供的是 ActorScope 作用域,返回的对象类型是 SendChannel。
kotlin
fun main() = runBlocking<Unit> {
// 启动协程,返回 SendChannel
val actorChannel: SendChannel<String> = actor(Dispatchers.Default) {
// 协程内部是接收端
for (data in channel) {
println("Received Message is: $data")
}
println("Actor is done.")
}
// 发送消息
launch(Dispatchers.IO) {
actorChannel.send("Message 1")
delay(100)
actorChannel.send("Message 2")
actorChannel.close()
}
}
运行结果:
less
Received Message is: Message 1
Received Message is: Message 2
Actor is done.