作者:Elye
去年年初的时候,我写了篇文章Kotlin Flow a much better version of Sequence?, 介绍了Flow,并与Sequence进行了比较。
现在除了Flow之外,我们还有ChannelFlow和CallbackFlow。它们与Flow有什么不同呢?为了了解它,让我们首先开始研究基本的Flow原理。
Flow
首先先看下面的代码,发送从1到5的数值。
fun main() = runBlocking {
flow {
for (i in 1..5) {
println("Emitting $i")
emit(i)
}
}
.collect { value ->
delay(100)
println("Consuming $value")
}
}
代码执行结果如下:
Emitting 1
Consuming 1
Emitting 2
Consuming 2
Emitting 3
Consuming 3
Emitting 4
Consuming 4
Emitting 5
Consuming 5
我们可以看到,每个emit都必须在下一个emit调用之前被消耗掉。这是因为没有缓冲区(buffer)来存储额外的emit,因此每个emit都必须排队,如下图所示:
没有任何通道的情况下,emit(发送)和consume(消耗)是同步进行的。这在我们想确保在前一个事件被消耗之前不emit的情况下是好的。但是,它会减慢emit(发送)过程。
增加缓冲区(Buffer)
为了使emit(发送)速度更快,我们可以给它增加一个缓冲区(使用buffer方法)。我们先试试0缓冲,代码如下:
fun main() = runBlocking {
flow {
for (i in 1..5) {
println("Emitting $i")
emit(i)
}
}.buffer(0)
.collect { value ->
delay(100)
println("Consuming $value")
}
}
执行结果如下:
Emitting 1
Emitting 2
Consuming 1
Emitting 3
Consuming 2
Emitting 4
Consuming 3
Emitting 5
Consuming 4
Consuming 5
虽然是0缓冲区,但是增加的缓冲区可以给缓冲区增加一个emit事件,如下图所示:
如果有 1 个缓冲区,结果会怎样?执行结果结果如下
Emitting 1
Emitting 2
Emitting 3
Consuming 1
Emitting 4
Consuming 2
Emitting 5
Consuming 3
Consuming 4
Consuming 5
这将允许在consume(消费)之前的第一个事件前完成3个emit(发送)。
使用.buffer(2),其执行结果如下
Emitting 1
Emitting 2
Emitting 3
Emitting 4
Consuming 1
Emitting 5
Consuming 2
Consuming 3
Consuming 4
Consuming 5
使用.buffer(3),我们可以直接完成5个事件的emit。其结果如下
Emitting 1
Emitting 2
Emitting 3
Emitting 4
Emitting 5
Consuming 1
Consuming 2
Consuming 3
Consuming 4
Consuming 5
除了Buffer之外,还有Conflate和CollectLatest,这篇文章Kotlin Flow Buffer is like A Fashion Adoption 对此进行了说明
ChannelFlow
上面我们已经了解了Flow的缓冲区,现在让我们看看Flow和ChannelFlow之间有什么区别吧。
ChannelFlow 是自带Buffer的Flow
执行如下代码:
fun main() = runBlocking {
channelFlow {
for (i in 1..5) {
println("Emitting $i")
send(i)
}
}
.collect { value ->
delay(100)
println("Consuming $value")
}
}
结果如下:
Emitting 1
Emitting 2
Emitting 3
Emitting 4
Emitting 5
Consuming 1
Consuming 2
Consuming 3
Consuming 4
Consuming 5
这看起来像使用了.buffer(3)的Flow。但是ChannelFlow有更多缓存区,因为默认情况下它有64个缓冲区。你也可以通过buffer
方法来设置缓冲区的大小
需要注意的是: channelFlow.buffer(0) 不等同于flow,而是类似 flow.buffer(0),更详细的解释请参考这篇文章Kotlin’s Channel Flow With Rendezvous Is Not The Same As Kotlin Flow
ChannelFlow 可以进行异步处理(例如 Merge)
如果我们有下面两个flow
val myAbcFlow = flow { ('A'..'E').forEach {
delay(50)
emit(it)
} }
val my123Flow = flow { (1..5).forEach {
delay(50)
emit(it)
} }
并且想合并它们,代码如下:
fun <T> Flow<T>.flowMerge(other: Flow<T>): Flow<T> = flow {
collect { emit(it) }
other.collect { emit(it) }
}
my123Flow.flowMerge(myAbcFlow).collect {
delay(50)
print("$it ")
}
执行结果如下所示:
1 2 3 4 5 A B C D E
可以看出,合并过程是按照顺序来的。collect{ emit(it) }
全部执行完,才会执行other.collect{ emit(it) }
。
我们不能使用 launch 来进行异步处理,因为它不是 CoroutineScope 的一部分。代码如下:
fun <T> Flow<T>.flowMerge(other: Flow<T>): Flow<T> = flow {
launch { // 这是错误的用法
collect { emit(it) }
}
other.collect { emit(it) }
}
下面的操作也是错误的
fun <T> Flow<T>.flowMerge(other: Flow<T>): Flow<T> = flow {
CoroutineScope(Dispatchers.Main).launch {
collect { emit(it) }
}
other.collect { emit(it) }
}
这种做法可以编译,但是在运行时会出错
FlowCollector is not thread-safe and concurrent emissions are prohibited.
To mitigate this restriction please use 'channelFlow' builder instead of 'flow'
这种情况下,你就可以使用channelFlow
fun <T> Flow<T>.channelMerge(other: Flow<T>): Flow<T> = channelFlow{
launch { // THIS IS OKAY
collect { send(it) }
}
other.collect { send(it) }
}
代码执行结果如下
A 1 B 2 C 3 D 4 E 5
launch { collect { send(it) }}
方法不再阻塞,因此other.collect{ send(it) }
可以并行执行。ChannelFlow 加快了执行流程,并让合并工作交替执行。
ChannelFlow能保持Open
当我们使用flow执行下面的代码
flow {
for (i in 1..5) emit(i)
}.collect { println(it) }
当执行完成,flow就关闭了。类似的,我们也可以使用ChannelFlow
channelFlow {
for (i in 1..5) send(i)
}.collect { println(it) }
当执行完成时,channelFlow也就关闭了。
如果数据来自另一个来源,而我们想保持flow open呢?也就是,数据可以在上面的Channel Flow lambda函数之外继续发送。
在正常情况下,这是不可能的。flow在执行完后将被关闭。但是,对于ChannelFlow,我们可以使用的awaitClose()
方法,代码如下:
channelFlow {
for (i in 1..5) send(i)
awaitClose()
}.collect { println(it) }
这个方法将防止函数在Lamda之后被关闭。为了实现在channelFlow lambda之后发送数据的能力,我们可以通过传递给一个变量函数将发送函数分配到外面,代码如下所示:
fun main(): Unit = runBlocking {
var sendData: suspend (data: Int) -> Unit = { }
var closeChannel: () -> Unit = { }
launch {
channelFlow {
for (i in 1..5) send(i)
sendData = { data -> send(data) }
closeChannel = { close() }
awaitClose {
sendData = {}
closeChannel = {}
}
}.collect { println(it) }
}
delay(10)
println("Sending 6")
sendData(6)
closeChannel()
}
我们现在可以在ChannelFlow
lambda之后使用sendData
发送数据。我们还可以稍后关闭通道,使用closeChannel
方法,它将在lambda中调用close()。
ChannelFlow 使用非挂起(suspend)函数
当我们使用flow时,我们必须使用emit,它是一个suspend的方法
public suspend fun emit(value: T)
在 ChannelFlow中,我们使用send
方法代替emit
方法,send
也是一个suspend方法
public suspend fun send(element: E)
如果限制了我们使用非suspend方法,在ChannelFlow中,我们可以使用trySend
public fun trySend(element: E): ChannelResult<Unit>
trySend
方法允许我们在不需要suspend的情况下发送数据。
fun main(): Unit = runBlocking {
var sendData: (data: Int) -> Unit = { } // Not suspending
var closeChannel: () -> Unit = { }
launch {
channelFlow {
for (i in 1..5) trySend(i)
sendData = { data -> trySend(data) }
closeChannel = { close() }
awaitClose {
sendData = {}
closeChannel = {}
}
}.collect { println(it) }
}
delay(10)
println("Sending 6")
sendData(6)
closeChannel()
sendData(7)
}
注意,trySend
在流被关闭的情况下使用ChannelResult返回状态,而不是崩溃。除了trySend
方法外,还有trySendBlocking
方法。如果缓冲区已满,trySend 将不发送数据,并执行下一次 trySend。如果缓冲区已满,trySendBlocking 将不会发送数据,而是等待缓冲区可用并再次 trySendBlocking。
public fun <E> SendChannel<E>.trySendBlocking(element: E)
: ChannelResult<Unit> {
trySend(element).onSuccess { return ChannelResult.success(Unit) }
return runBlocking {
val r = runCatching { send(element) }
if (r.isSuccess) ChannelResult.success(Unit)
else ChannelResult.closed(r.exceptionOrNull())
}
}
CallbackFlow
当我们在ChannelFlow里面使用awaitClose()时,我们是期望设置其lambda的回调。为了确保我们记得使用awaitClose(),我们可以使用CallbackFlow代替channelFlow,CallbackFlow本质上是channelFlow,CallbackFlow强制要求使用awaitClose()。
如果我们不在CallbackFlow使用awaitClose(),你会得到一个运行时错误,即
Exception in thread "main" java.lang.IllegalStateException: 'awaitClose { yourCallbackOrListener.cancel() }' should be used in the end of callbackFlow block.
Otherwise, a callback/listener may leak in case of external cancellation.
更多见解,请查看下面的文章。Keep Your Kotlin Flow Alive and Listening With CallbackFlow
我们还需要使用flow吗?
最开始,我认为flow只能实现channelFlow的部分功能,channelFlow是flow的一个超集。然后,我了解到flow默认情况下的行为与channelFlow略有不同,正如这篇文章所描述的那样Kotlin’s Channel Flow With Rendezvous Is Not The Same As Kotlin Flow
除此以外,与channelFlow相比,flow的重量也更轻。如下代码,如果我们仅仅比较emit
和send
fun main() = runBlocking {
val x = 10000000
val time = measureNanoTime {
channelFlow {
for (i in 1..x) {
send(i)
}
}.collect {}
}
println(time)
}
fun main() = runBlocking {
val x = 10000000
val time = measureNanoTime {
flow {
for (i in 1..x) {
emit(i)
}
}.collect {}
}
println(time)
}
结果如下图
我们可以明显看到,flow在默认情况下比channelFlow的重量更轻。所以,除非你需要 channelFlow 的花哨功能,否则请使用 Flow。
总结
1. 如果flow满足你的要求,请使用flow
2. 如果你需要在flow里面使用缓冲和异步,使用ChannelFlow
3. 如果你需要在flow外部发送数据,使用带有 awaitClose 的 CallbackFlow。