开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情
续接上文: 轻松搞定Kotlin的Flow, ChannelFlow和CallbackFlow - 1
ChannelFlow
现在我们已经了解了Flow的缓冲区, 让我们看看Flow和ChannelFlow之间有什么区别.
1. ChannelFlow是有缓冲区的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
看起来像常规的flow使用了.buffer(3). 但是它有更多的缓冲, 默认大小是64.
要想增(或减)缓冲区, 可以在channelFlow上使用buffer.
2. ChannelFlow能够执行异步处理(比如合并)
假设我有下面的输入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 { // ERROR HERE.
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) }
因此加快了整个过程, 同时使合并工作交替进行.
3. ChannelFlow能够保持开放
当我们执行以下流程时:
flow {
for (i in 1..5) emit(i)
} .collect { println(it) }
完成. 结束.
同样地, 我们也可以将此应用于ChannelFlow:
channelFlow {
for (i in 1..5) send(i)
} .collect { println(it) }
完成. 结束.
如果数据来自另一个来源, 而我们想保持流量开放呢? 你想让它变得"更热", 也就是说, 数据可以在上面的ChannelFlow lambda函数之外继续来.
在正常情况下, 这是不可能的.在完成Channel Flow lambda后, Channel Flow将被关闭.
然而, 对于Channel Flow, 我们可以在Lambda的末尾添加一个特殊的函数awaitClose().
channelFlow {
for (i in 1..5) send(i)
awaitClose()
} .collect { println(it) }
这将防止该函数在Lamda之后也被关闭.
为了扩展在channelFlow在lambda之后发送数据的能力, 我们可以将send函数分配给外面的一个变量函数, 如下所示, 即sendData:
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函数舶关闭channel, closeChannel将在lambda内部调用close().
4. ChannelFlow能够发送非挂起数据
我们在用flow的时候, 必须得用emit, 因为它是挂起函数.
public suspend fun emit(value: T)
在channelFlow中, 我们使用了send, 同样也是因为它是挂起函数.
public suspend fun send(element: E)
如果我们想发送不在暂停模式下的数据, 这是有限制的.
在channelFlow中, 我们有trySend, 但它却不是挂起函数.
public fun trySend(element: E): ChannelResult<Unit>
它将允许人们在不需要挂起函数的情况下进行发送.
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<Unit>返回状态而不是崩溃.
除了trySend之外, 还有trySendBlocking. 它是:
public fun <E> SendChannel<E>.trySendBlocking(element: E) : ChannelResult<Unit> {
/*
* Sent successfully -- bail out.
* But failure may indicate either that the channel it full
* or that
* it is close. Go to slow path on failure to simplify the
* successful path and
* to materialize default exception.
*/
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())
}
}
借此:
trySend如果缓冲区满了, 将不发送数据, 并执行下一次trySend.trySendBlocking如果缓冲区满了, 将不发送数据, 而是等待缓冲区可用时再trySendBlocking.
CallbackFlow
对于channelFlow, 当我们使用awaitClose()时, 表明我们希望它的行为是对其lambda的回调.
如果我们需要设置任何回调, 对我们来说, 设置awaitClose()是很重要的.
为了确保我们记得使用awaitClose(), 我们可以使用callbackFlow来代替channelFlow, 而callbackFlow本质上是channelFlow, 并强制使用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.
如果channelFlow比flow好得多, 为什么还要用flow?
最初, 我在想, flow只能做channelFlow能做的事情的一个子集. 认为channelFlow是flow的一个超集.
然后我了解到flow在默认情况下与channelFlow的行为略有不同, 正如下面的文章所描述的那样:
除此之外, 与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重量轻.
所以简而言之, 使用flow, 除非你需要channelFlow的铃声和口哨.
TL; DR;
如果flow满足你的需要, 就使用flow.
如果你在flow内需要缓冲区和异步, 使用channelFlow.
如果你需要在flow之外发送数据, 使用callbackFlow与awaitClose.
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情