前面我们讲解的 Channel 的相关知识。Channel 是一个跨协程传递数据的工具,属于底层工具,不是拿来直接用的,尤其是在近些年 Flow API 被推出以及逐渐完善之后,Channel 都不太适合当做一个上层的功能性工具来用了。如果需要事件流或数据流,还是用 Flow 比较好。Channel 现在更多是作为 Flow API 的一个关键的底层支撑而存在,它给 Flow 提供了跨协程的能力。从功能上说,Flow 更完整,使用体验更顺畅。
这一篇我们就开始了解 Flow 的相关内容。
1、Flow 的功能定位
Flow API 具体可以分为三类:
- 普通的 Flow API:最早诞生,属于数据流
- SharedFlow:事件流
- StateFlow:特殊的 SharedFlow,提供状态订阅而不是事件订阅。但实际上,状态订阅本身也是事件订阅,因为状态的改变就是一种事件。但由于状态订阅这种订阅类型太过于常见与实用,所以协程单独把这种订阅给做了一份实现
数据流:关注的是同一个协程下的数据序列的持续发送和处理。
事件流:关注的是事件序列向多个订阅者的、一对多的、跨协程的通知。
由于 Flow 的功能与 Sequence 非常相似,或者说 Flow 可以看作协程版的 Sequence,因此我们对 Flow 的讲解,先从 Sequence 开始。
1.1 Sequence
Sequence 的简单使用
Sequence 是一种让我们可以像队列一样按顺序提供数据,并且按照提供的顺序来获取数据的 API。虽然说 Sequence 像队列,但它跟队列有着本质的不同 ------ Sequence 不是一种数据结构,而是一种机制。它让我们提供数据规则,而不是数据本身。这样用 Sequence 提供的数据,实际上是动态生产的,用完一条才生产下一条。而不是像队列那样,把所有数据都提前准备好,然后一条一条取出。
具体的使用方法,通过 sequence 函数,在 block 内通过 yield() 生产数据:
kotlin
val nums = sequence {
yield(1)
yield(2)
}
注意,协程当中也有一个同名的 yield() 函数,用于在当前时间片未到时主动放弃时间片,定位是与 Thread.yield() 类似的。与 Sequence 的 yield() 不是同一个函数,Sequence 的 yield() 有参数而协程的没有,不要混淆。
通过 for 循环遍历取出元素:
kotlin
for(num in nums) {
println(num)
}
与 List 的类比
List 也有类似的用法:
kotlin
val list = buildList {
add(1)
add(2)
}
for(num in list) {
println(num)
}
你也可以将 List 通过 List.asSequence() 转换为一个生产 List 内数据的 Sequence。
Sequence 与 List 的最大区别是:Sequence 是惰性(Lazy)的,生产策略是拖延的。上面返回的 list 里面包含两个元素,而 nums 只包含代码块,即生产策略,直到被遍历时,才开始执行代码块生产数据。即用的时候才生产数据,而不是声明之后就生产。并且,是用到一条数据才生产一条,在执行 for(num in nums)
遍历时,遍历到第一个元素时,Sequence 生产第一个数据,在进行数据处理过程中时,Sequence 不会生产第二条数据,直到 for 循环开始遍历第二条元素时才开始生产。
Sequence 惰性生产数据的好处是,可能会因为满足生产过程中的某些停止条件(比如遍历到 i = 10 时就 break 结束生产过程),而节省生产数据的总时间。并且,这种生产一条使用一条的特性使得通过 Sequence 获得一条数据的时间要比 List 快,开始操作具体数据的时间比 List 快(当然,对于相同数量的数据,总的生产时间是相同的)。Sequence 适用于那种有一条数据就赶紧处理一条的业务场景,而不是把所有数据全处理完才有意义的场景。
Sequence "不支持"协程
sequence 的 block 参数是生产数据的规则:
kotlin
@SinceKotlin("1.3")
@Suppress("DEPRECATION")
public fun <T> sequence(@BuilderInference block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence { iterator(block) }
这个 block 虽然有 suspend 修饰,表面上看起来支持挂起函数,但是接收者 SequenceScope 被标记了 @RestrictsSuspension 注解:
kotlin
@RestrictsSuspension
@SinceKotlin("1.3")
public abstract class SequenceScope<in T> internal constructor() {
...
}
这样就限制该 block 只能是 Sequence 自己的挂起函数,如 yield()、yieldAll(),其他挂起函数不行。因此,实际上,Sequence 相当于不支持协程。
Kotlin 团队使用协程完成 Sequence 的业务逻辑,但是希望 Sequence 的协程逻辑是独立的、与世隔绝的,不与使用者创建的协程产生任何的联系,因此使用 @RestrictsSuspension 注解,只能调用自己的挂起函数,调用别的就报错。
1.2 引入 Flow
如果想在 Sequence 内使用协程,调用挂起函数该如何处理?这时就轮到 Flow 登场了。Flow 相当于是一个协程版的 Sequence,可以在内部调用挂起函数:
kotlin
fun main() = runBlocking<Unit> {
val numFlow = flow {
// 使用挂起函数 emit 发送数据
emit(1)
emit(2)
}.map { "number $it" } // Flow 支持各种操作符
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
// 在指定协程内获取并遍历数据,Flow 不能用 for 循环遍历,而是用 collect
numFlow.collect {
println(it)
}
}
delay(3000)
}
最后我们做一个小结:Sequence 的定位是提供一个边生产边消费的数据序列,而 Flow 的定位就是(支持)协程版的 Sequence。Flow 的应用场景是提供一个支持挂起函数的数据流,比如一个持续获取网络数据的数据流。
2、Flow 的工作原理和应用场景
2.1 Flow 的原理和本质
Flow 的原理非常简单,它相当于把 flow 函数内的代码挪到 collect() 调用的地方来执行。
来看个例子:
kotlin
fun main() = runBlocking<Unit> {
val numFlow = flow {
emit(1)
delay(100)
// 延迟 100ms 发送第二条数据
emit(2)
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
numFlow.collect {
println("A: $it")
}
}
scope.launch {
// 延迟 50ms 才开始接收数据,看能否收到第一条数据
delay(50)
numFlow.collect {
println("B: $it")
}
}
delay(3000)
}
第二个 launch 在 flow 发射完第一条数据之后才开始遍历数据,但也能收到所有的数据:
A: 1
B: 1
A: 2
B: 2
将示例代码进行等价替换就容易理解为什么会是这样的结果:
kotlin
fun main() = runBlocking<Unit> {
val numFlow = flow {
emit(1)
delay(100)
emit(2)
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
println("A: 1") //emit(1)
delay(100)
println("A: 2") //emit(2)
/*numFlow.collect {
println("A: $it")
}*/
}
scope.launch {
delay(50)
println("B: 1") //emit(1)
delay(100)
println("B: 2") //emit(2)
/*numFlow.collect {
println("B: $it")
}*/
}
delay(3000)
}
用 flow 内的代码块替换掉 collect() 的代码,同时将发射数据的代码替换为对数据进行具体处理的代码,形成上述的伪代码,自然容易看出得出上述结果的原因了。
Flow 的本质:Flow 设定好一套逻辑,在每个 collect() 调用处重复执行一遍这套逻辑。在逻辑里安插发送数据的节点,而 collect() 在执行这套逻辑时,对每一条数据,都会执行自己设定好的数据处理逻辑。
简言之,Flow 对象提供数据流的生产逻辑,在收集流程里执行这套生产逻辑并处理每条数据。Flow 是一个数据流工具指的就是这个。
2.2 Channel 和 Flow 的"冷"与"热"
Kotlin 官方文档中说 Channel 是热(hot)的,Flow 是冷(cold)的,指的是数据生产是独立的,还是跟数据收集有关的:
- 热,指的是没收集数据的时候也可以生产数据
- 冷,指的是开始收集数据的时候才开始生产数据
Channel 有它自己独立的生产线,调用一次 send() 就生产一条数据,与是否调用 receive() 来取数据是无关的。
Flow 则不然,flow() 存的只是生产规则,而真正的生产一定是在每次调用 collect() 时才开始的,每次 collect() 都会有一次完整的生产流程。
2.3 Flow 的适用场景
其实 Sequence 跟 Flow 的应用场景几乎是一样的,只不过一个是同步的,一个是挂起式的。
这里我们直接说 Flow。什么时候会需要 Flow 呢?当需要连续地处理同类型的数据时,可以......先直接处理。比如有一个获取天气信息的挂起函数:
kotlin
suspend fun getWeather() = withContext(Dispatchers.IO) {
// 用 delay 模拟通过网络获取天气数据的过程,最后返回一个结果字符串
delay(100)
"Sunny"
}
想要每分钟获取天气信息再打印出来:
kotlin
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val job = scope.launch {
while (true) {
println("Weather: ${getWeather()}")
delay(60_000)
}
}
job.join()
}
这样看似乎不要 Flow,但假如我需要对功能进行拆分,把数据获取和数据处理的功能拆开,Flow 就派上用场了。
假设在界面模块,有一个显示天气数据的函数。该函数希望外界提供天气信息的持续获取的功能。外界怎么获取天气、多久获取一次我都不想管,只想持续地拿到最新的天气数据然后更新到界面中。这时要用 Flow 作为函数参数:
kotlin
suspend fun showWeather(flow: Flow<String>) = flow.collect {
println("Weather: $it")
}
创建参数所需的 Flow 对象:
kotlin
val weatherFlow = flow {
while (true) {
emit(getWeather())
delay(60_000)
}
}
调用 showWeather() 显示天气:
kotlin
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val job = scope.launch {
showWeather()
}
job.join()
}
通过例子可以看到,持续提供数据这种形式,就是所谓的数据流。而需要把数据流的生产和消费功能拆开的业务场景就是 Flow 的用途所在。
2.4 collect() 会"把协程卡死"
回看上一节的 showWeather(),它的 collect() 是一个挂起函数,在其执行完之前,协程内的后续代码不会被执行。如果 showWeather() 在 collect() 后面再加一些其他代码,它们在 collect() 收集完全部数据之前就得不到执行。
这里要注意,严格来讲,collect() 是数据流中的收集,而不是事件流中的订阅。因此,collect() 在收集完所有数据之后再执行后续代码是合理的。
此外,由于数据流与事件流并没有严格的界限划分,因此你将 collect() 就当作一个事件订阅函数,也没什么问题。只不过,从事件订阅的角度来看 collect(),在事件订阅完成后,理应继续执行后续代码,而不是卡在这里,让后续代码无法执行。这是 collect() 作为事件订阅的一个不足,但我们可以通过合理的安排代码结构来消除这种不足,毕竟业务代码是由我们程序员自己掌控的。如果真的出现业务需要在 collect() 之后还有其他代码,你可以将 collect() 放到一个单独的协程中,这样就不会卡住后续代码了。
3、Flow 的创建
本节主要介绍创建 Flow 会使用的 API。
前面我们只是通过 flow() 来创建 Flow 对象,自己定制生产逻辑与发送数据。而 Flow 其实还提供了其它创建 Flow 的函数,可以方便地把其他类型的对象直接转换成 Flow。
3.1 flowOf()
提供一串数据给 flowOf(),进而生成一个 Flow 对象,Flow 在生产时会直接把这些数据依次发送出来:
kotlin
fun main() = runBlocking<Unit> {
val flow = flowOf(1, 2, 3)
val scope = CoroutineScope(EmptyCoroutineContext)
val job = scope.launch {
flow.collect {
println("Flow: $it")
}
}
job.join()
}
查看 flowOf() 的源码可以发现,它其实就是一个便捷函数,创建 Flow 对象并逐个发射提供的数据:
kotlin
public fun <T> flowOf(vararg elements: T): Flow<T> = flow {
for (element in elements) {
emit(element)
}
}
3.2 asFlow()
与 flowOf() 类似,也是提供数据给函数,函数帮你创建 Flow 对象。区别在于,asFlow() 是一个扩展函数,可以把各种类型的对象转换为 Flow 对象,比如 List、Set 和 Sequence 都可以通过 asFlow() 生成对应的 Flow 对象:
kotlin
val flow1 = listOf(4, 5, 6).asFlow()
val flow2 = setOf(1, 2, 3).asFlow()
val flow3 = sequenceOf(1, 2, 3).asFlow()
val channel = Channel<Int>()
val flow4 = channel.consumeAsFlow()
val flow5 = channel.receiveAsFlow()
val scope = CoroutineScope(EmptyCoroutineContext)
val job = scope.launch {
flow2.collect {
println("Flow: $it")
}
}
job.join()
这里 asFlow() 是作为 Iterable 的扩展函数:
kotlin
public fun <T> Iterable<T>.asFlow(): Flow<T> = flow {
forEach { value ->
emit(value)
}
}
3.3 consumeAsFlow() 与 receiveAsFlow()
通过 Channel 也可以生成 Flow 对象:
kotlin
fun main() = runBlocking<Unit> {
val channel = Channel<Int>()
val flow1 = channel.consumeAsFlow()
val flow2 = channel.receiveAsFlow()
val scope = CoroutineScope(EmptyCoroutineContext)
val job1 = scope.launch {
flow2.collect {
println("Flow2-1: $it")
}
}
val job2 = scope.launch {
flow2.collect {
println("Flow2-2: $it")
}
}
channel.send(1)
channel.send(2)
channel.send(3)
channel.send(4)
job1.join()
job2.join()
}
由 Channel 提供数据的 Flow,从上游看,它是由 Channel 提供热流的数据,交给下游的 Flow,仍是直到调用 collect() 的时候才开始生产数据。整体是一个双阶段的生产流程,上游生产自己的数据,下游从上游拿到数据后,会先 hold 住这些数据,步继续往下释放,直到下游的下游索要数据时再去释放。
上述代码还需注意,两个协程中的 flow2 会瓜分 Channel 发出的数据:
Flow2-1: 1
Flow2-2: 2
Flow2-1: 3
Flow2-2: 4
这与 Channel 的性质是一致的,Channel 发出的数据只能被接收一次。
通过 consumeAsFlow() 生成的 Flow 对象有一个 consume 的概念,这个流只能被消费一次,如果在这个流上多次调用 collect(),就会抛出异常:
kotlin
fun main() = runBlocking<Unit> {
val channel = Channel<Int>()
val flow1 = channel.consumeAsFlow()
val flow2 = channel.receiveAsFlow()
val scope = CoroutineScope(EmptyCoroutineContext)
val job1 = scope.launch {
flow1.collect {
println("Flow2-1: $it")
}
}
val job2 = scope.launch {
flow1.collect {
println("Flow2-2: $it")
}
}
channel.send(1)
channel.send(2)
channel.send(3)
channel.send(4)
job1.join()
job2.join()
}
flow1 调用了两次 collect(),抛出异常:
Exception in thread "main" kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=JobImpl{Cancelling}@4ec6a292
Caused by: java.lang.IllegalStateException: ReceiveChannel.consumeAsFlow can be collected just once
...
如果是进行应用层开发,几乎用不到 Channel,也就用不到这对函数。
3.4 channelFlow() 与 callbackFlow()
channelFlow() 是使用 Channel 作为生产工具的 Flow,它创建的 Flow 直到 collect() 的时候才会创建 Channel,开始生产。
channelFlow() 的 block 内可以使用 send() 发送数据,更重要的是,可以在这里开启协程:
kotlin
fun main() = runBlocking<Unit> {
val channelFlow = channelFlow {
// channelFlow 内开启协程发送数据
launch {
delay(2000)
send(2)
}
delay(1000)
send(1)
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
channelFlow.collect {
println("channelFlow: $it")
}
}
delay(3000)
}
普通的 Flow 允许开启子协程,但是不允许在这个子协程内发送数据:
kotlin
fun main() = runBlocking<Unit> {
val flow = flow {
launch {
delay(2000)
emit(2)
}
delay(1000)
emit(1)
}
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch {
flow.collect {
println("channelFlow: $it")
}
}
delay(3000)
}
运行会抛出异常:
channelFlow: 1
Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
Emission from another coroutine is detected.
...
意思是你不能从另一个协程中发射数据。flow 的代码块是在哪个协程中运行,emit() 就要在哪个协程中发射数据。因此 launch() 内的 emit() 由于更换了协程,所以导致异常抛出。
两相比较,可以跨协程才是 channelFlow() 真正的用途。除此之外,channelFlow() 还可与传统的回调式代码进行对接:
kotlin
fun main() = runBlocking<Unit> {
val channelFlow = channelFlow { // this:ProducerScope
gitHub.contributionsCall("square", "retrofit")
.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(
call: Call<List<Contributor>>,
response: Response<List<Contributor>>
) {
// 没在协程里,不能用 send
trySend(response.body()!!)
}
override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
// 遇到异常就取消
cancel(CancellationException(t.message))
}
})
}
val scope = CoroutineScope(EmptyCoroutineContext)
val job = scope.launch {
channelFlow.collect {
println("channelFlow: $it")
}
}
job.join()
}
但是这样写,有一个问题:channelFlow 的代码块内部本质上是在执行 produce(),也就是一个协程,在执行完 enqueue() 之后协程也就结束了。因此等 Callback 的回调函数被调用时,协程早已结束,trySend() 就会失败,运行上述代码就不会打印任何东西。
解决方案就是想办法让协程不结束,在协程内使用 awaitClose() 会一直掐住协程让其挂起,直到你手动调用 cancel() 或 close() 关闭协程:
kotlin
fun main() = runBlocking<Unit> {
val channelFlow = channelFlow {
gitHub.contributionsCall("square", "retrofit")
.enqueue(object : Callback<List<Contributor>> {
override fun onResponse(
call: Call<List<Contributor>>,
response: Response<List<Contributor>>
) {
// 没在协程里,不能用 send
trySend(response.body()!!)
// 回调函数执行完毕,关闭协程
close()
}
override fun onFailure(call: Call<List<Contributor>>, t: Throwable) {
// 遇到异常就取消
cancel(CancellationException(t.message))
}
})
// 让协程挂起等待回调函数执行
awaitClose()
}
val scope = CoroutineScope(EmptyCoroutineContext)
val job = scope.launch {
channelFlow.collect {
println("channelFlow: $it")
}
}
job.join()
}
这样即可成功打印数据:
channelFlow: [Contributor(login=JakeWharton, contributions=1350), Contributor(login=swankjesse, contributions=232), Contributor(login=renovate[bot], contributions=212), Contributor(login=squarejesse, contributions=49), Contributor(login=pforhan, contributions=48), ...
实际上,当需要与回调 API 进行交互时,更合适的是使用 callbackFlow()。callbackFlow() 内部其实就是一个 channelFlow(),只不过它会强制调用 awaitClose(),如果忘记调用会抛异常提醒你,其余逻辑与 channelFlow() 完全相同。
如果只进行单次回调函数的调用,那么完全可以使用前面介绍过的 suspendCancellableCoroutine() 这个挂起函数来做,要比 callbackFlow() 更方便。但如果需要的是连续的回调数据,即数据流,那么还是要使用 callbackFlow()。
callbackFlow() 可以看作是 Flow 版的 suspendCancellableCoroutine(),二者都用来对接回调函数。