Flow 的定位:协程版的 Sequence
要理解 Flow 是什么,先要理解 Sequence(序列)。
Sequence 和 Queue 一样,都可用于按顺序提供数据,但它并不是一种数据结构,而是一种机制。
为什么这么说呢?
它内部没有存储数据,只是存储了提供数据的规则。当需要提供数据时,会动态生产,并且是"用完一条才生产下一条"。
kotlin
fun main() = runBlocking<Unit> {
val numSeq = sequence { // 创建 Sequence 对象
println("Producing 1")
yield(1) // 生产数据
println("Producing 2")
yield(2)
println("Producing 3")
yield(3)
}
launch {
for (num in numSeq) {
println("Consuming $num")
delay(1000)
if (num == 2) {
break
}
}
}
}
运行结果:
less
Producing 1
Consuming 1
Producing 2
Consuming 2
可以看到,因为提前结束了循环,所以并不会打印 "Producing 3",这展示了 Sequence 的惰性求值特性。
并且 Sequence 和 List 一样,都可用来处理数据集合。不过 List 会立即执行每个操作,中间会产生新的 List。而 Sequence 是惰性求值,它会先构建一个操作链,然后让生产的每个数据执行完所有操作步骤。
kotlin
fun main() = runBlocking<Unit> {
// Sequence 惰性求值
val seq = sequence {
println("Sequence producing 1")
yield(1)
println("Sequence producing 2")
yield(2)
}
seq.take(1).forEach {
println("Consuming Sequence: $it")
}
// List 立即求值
val list = buildList {
println("List producing 1")
add(1)
println("List producing 2")
add(2)
}
list.take(1).forEach { println("Consuming List: $it") }
}
运行结果:
less
Sequence producing 1
Consuming Sequence: 1
List producing 1
List producing 2
Consuming List: 1
这同样清晰地展示 Sequence 按需生产的惰性特性。
那么 Sequence 有什么用处吗?
关键在于它是惰性的,它的生产策略是消极的。如果遍历过程中,停止遍历,这样可以减少生产耗时。如果数据的获取是持续的,只需要这样:
kotlin
fun main() = runBlocking<Unit> {
val numSeq = sequence {
while (true) {
yield(getData()) // 实际上这行会报错
}
}
launch {
for (num in numSeq) {
println("Consuming $num")
delay(1000)
}
}
}
// 模拟耗时的挂起操作
suspend fun getData(): Int = withContext(Dispatchers.IO) {
delay(1000)
Random.Default.nextInt()
}
虽然 sequence 构建器代码块内部允许我们使用挂起函数,比如调用 yield() 生产数据。但由于 SequenceScope,我们只能调用这个作用域内部的 yield 和 yieldAll 挂起函数(因为 @RestrictsSuspension 注解),并不能调用 delay 等挂起函数(包括我们之前定义的 getData())。
kotlin
@SinceKotlin("1.3")
@Suppress("DEPRECATION")
public fun <T> sequence(@BuilderInference block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence { iterator(block) }
但 flow() 中拥有完整的挂起环境,所以说:Sequence 的定位是一个惰性的同步数据序列(无法调用外部的挂起函数),Flow 的定位则是一个支持挂起函数的、惰性的异步数据流。
kotlin
fun main() = runBlocking<Unit> {
val numSeq = flow {
while (true) {
// 发送数据
emit(getData())
}
}
launch {
numSeq.collect { // 遍历 Flow
println("Consuming $it")
delay(1000)
}
}
}
suspend fun getData(): Int = withContext(Dispatchers.IO) {
delay(1000)
Random.Default.nextInt()
}
运行结果:
less
Consuming -680817724
Consuming 611886988
Consuming -695569431
...
冷流:Flow 的核心原理
现在,我们来讲讲 Flow 的核心原理 "冷流"。
首先有个问题:Flow 生产了第一条数据后,在新的协程中收集它,那么在这个协程中能不能获取到第一条数据?
kotlin
val myFlow = flow {
emit(1)
delay(1000)
emit(2)
}
fun main() = runBlocking<Unit> {
println("Calling collect A...")
launch {
myFlow.collect {
println("A received $it")
}
}
delay(500)
println("Calling collect B...")
launch {
myFlow.collect {
println("B received $it")
}
}
}
运行结果:
less
Calling collect A...
A received 1
Calling collect B...
B received 1
A received 2
B received 2
flow{...} 中的代码提供数据流的生产逻辑,在定义时并不会执行任何操作,只有当 collect 被调用时,它才会被执行。并且每一次调用 collect,都会触发一套全新、独立的生产流程。
实际上,launch { myFlow.collect { println("A received $it") } } 在概念上等同于:
kotlin
launch {
// Flow 代码块开始
val value1 = 1 // 对应 emit(1)
println("A received $value1") // 对应 collect 代码块的执行
delay(1000)
val value2 = 2
println("A received $value2")
// Flow 代码块结束
}
每一个 collect 调用都相当于执行了这样一套独立的逻辑。
这就是冷流,它与热流(如 Channel、SharedFlow)不同,热流的数据生产流程是独立的,与收集者无关。
Flow 的应用场景:解耦
当需要处理数据流时,就需要用到 Flow 吗?
不是的,我们只需要循环获取数据,然后处理即可。
kotlin
fun main() = runBlocking<Unit> {
launch {
while (isActive) {
val data = getData()
processData(data)
}
}
}
suspend fun getData() = withContext(Dispatchers.IO) {
"data"
}
fun processData(data: String) {
println(data)
}
但如果要将数据的获取和处理功能拆分,就需要用到 Flow,例如:
kotlin
// 生产者
fun fetchWeatherUpdates(): Flow<String> = flow {
val weather = listOf("sunny", "cloudy", "rainy")
while (true) {
val latestWeather = withContext(Dispatchers.IO) {
delay(500)
weather.random()
} // 挂起函数
emit(latestWeather)
delay(60_000) // 每分钟更新
}
}
// 消费者
suspend fun getWeatherUpdates(weatherFlow: Flow<String>) {
weatherFlow.collect { weather ->
println("Now weather is $weather") // 更新 UI
}
}
fun main() = runBlocking<Unit> {
val flow: Flow<String> = fetchWeatherUpdates()
launch {
getWeatherUpdates(flow)
}
}
消费者并不需要关心数据的来源,生产者不关心数据的处理。
Flow 的创建
除了之前的 flow() 外,Flow 最常见的创建方式还有 flowOf 和 asFlow。
flowOf()
它会将你提供的一组数据转换为 Flow,并将这些数据一次性发送出来。例如:
kotlin
fun main() = runBlocking<Unit> {
// 创建按顺序发送 1, 2, 3 的 Flow
val flow = flowOf(1, 2, 3)
flow.collect {
println(it)
}
}
运行结果:
less
1
2
3
我们来看看 flowOf 的内部实现,发现它只是在 flow{...} 的基础上,将传入的每个元素调用 emit() 发射出去罢了。
kotlin
// Builders.kt
public fun <T> flowOf(vararg elements: T): Flow<T> = flow {
for (element in elements) {
emit(element)
}
}
asFlow()
asFlow() 是一个扩展函数,它可以将现有的集合或序列转换为 Flow,原理和 flowOf 类似。
kotlin
fun main() = runBlocking<Unit> {
// 将 List 转换为 Flow
listOf("A", "B", "C").asFlow()
.collect { println(it) }
// 将 Sequence 转换为 Flow
sequenceOf("X", "Y", "Z").asFlow()
.collect { println(it) }
}
运行结果:
less
A
B
C
X
Y
Z
consumeAsFlow() 和 receiveAsFlow()
这两个函数可以将 Channel 转为 Flow。
但由于背后生产数据的是 Channel,所以数据会瓜分给所有的消费者。
kotlin
fun main() = runBlocking<Unit> {
val channel = Channel<Int>()
launch {
var count = 0
while (isActive) {
channel.send(count++)
delay(500)
}
}
delay(2000)
val flow = channel.receiveAsFlow()
launch {
flow.collect {
println("A received: $it")
}
}
delay(500)
launch {
flow.collect {
println("B received: $it")
}
}
delay(2000)
channel.cancel()
}
运行结果:
less
A received: 0
A received: 1
B received: 2
A received: 3
B received: 4
由于数据被瓜分,所以当 collect 的调用超过一次时,会导致每个消费者接收不到完整的数据序列。
所以有了 consumeAsFlow(),当它创建出的 Flow 调用 collect 被收集时,内部会标记为已消费,如果再次调用 collect 去收集,会抛出 IllegalStateException 异常。
例如将上述代码中的 .receiveAsFlow() 换成 .consumeAsFlow(),运行结果会是:
less
A received: 0
Exception in thread "main" java.lang.IllegalStateException: ReceiveChannel.consumeAsFlow can be collected just once
这个异常相当于是一种提醒。
channelFlow 和 callbackFlow
channelFlow() 也可创建 Flow,它使用 Channel 来生产数据。
与 receiveAsFlow() 不同的是:直到 collect 收集时,它才会创建 Channel 对象开始生产数据,也就是说,多次创建便会创建多个 Channel,所以它创建的也是"冷流"。
kotlin
fun main() = runBlocking<Unit> {
val flow = channelFlow {
for (i in 1..5) {
send(i) // 内部使用的是 Channel,所以调用 Channel 的 send 方法来生产
}
}
launch {
flow.collect {
println(it)
}
}
}
不过,channelFlow() 的关键用途并不在于创建 Flow。在了解它的用途前,我们先来看看 Flow 的 emit 的跨界问题。
emit 的跨界问题
如果在 flow 的代码块中启动一个新的协程来 emit 数据,会抛出 IllegalStateException 异常,简略的异常信息为:
less
Flow invariant is violated:
Emission from another coroutine is detected.
为什么会有这个限制?
这是为了保护消费者的上下文,让代码的行为逻辑与开发者预期一致。
kotlin
val myFlow = flow {
withContext(Dispatchers.IO) {
emit(1) // 在 IO 线程发射
}
}
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default) { // 请假设这是主线程 Dispatchers.Main
myFlow.collect { data ->
// 我们期望这里是主线程,但实际却运行在 IO 线程
println(data) // 更新 UI
}
}
}
我们知道 flow{...} 代码块中的代码会放在 collect 中执行,如果不进行限制,就可能会导致上述问题:collect 中更新 UI 的代码运行在了 Dispatchers.IO 线程,应用崩溃了。
为此,flow 块中的 emit 必须运行在调用 collect 的那个协程的上下文中,这样让 Flow 变得更加安全以及更加符合开发者的直觉。
channelFlow 的关键用途:跨协程生产
再说回 channelFlow,它的关键用途在于它允许我们跨界,也就是允许在不同的协程中生产数据。
为什么它能做到?因为 Channel 本身就是一个可跨协程通信的组件。
kotlin
val myFlow = channelFlow {
withContext(Dispatchers.IO) {
send(1) // 在 IO 线程发射
}
}
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default) { // 请假设这是主线程
myFlow.collect { data ->
println(data) // 完全可以安全地更新 UI
}
}
}
callbackFlow:对接传统回调
channelFlow 还可以用来对接传统的回调,只需这样:
kotlin
fun LocationManager.locationFlow(): Flow<Location> = channelFlow {
// 定义回调
val listener = object : LocationListener {
override fun onLocationChanged(location: Location) {
// 在回调中使用 trySend 发送数据,因为当前并没有在协程环境中,所以不能使用 send
trySend(location)
}
override fun onProviderDisabled(provider: String) {
// 当服务不可用时,关闭 Channel
close(IllegalStateException("Provider $provider disabled"))
}
}
// 注册回调
requestMyLocationUpdates(listener)
// 挂起生产者协程,直到 Flow 被外部取消
awaitClose {
// 在这里执行清理工作:注销回调
removeMyLocationUpdates(listener)
}
}
为什么需要 awaitClose()?
channelFlow 的代码块是一个协程,如果只是注册了回调,那么这个代码块会立即执行完毕并退出,Flow 也会随之关闭,回调也就不会被接收了。为此,我们必须调用 awaitClose(),它会将生产者协程挂起,直到 Flow 被消费者取消,或者被生产者关闭。
无论
Flow是如何结束的,awaitClose的代码块一定会被执行,我们可以在这执行清理逻辑。
而 callbackFlow 只是 channelFlow 的一个特殊版本,它强制要求调用 awaitClose()。如果不调用,会抛出异常。
Flow 的收集
collectIndexed
如果我们需要在收集数据时,知道当前数据的索引,可以使用 collectIndexed。
kotlin
fun main() = runBlocking<Unit> {
flowOf("A", "B", "C").collectIndexed { index, value ->
println("Index $index: Value $value")
}
}
收集与 launchIn
前面我们已经知道 collect 是一个挂起函数,如果数据流是无限的,那么它会一直挂起。
这会导致后续代码无法执行:
kotlin
val weatherFlow = flow {
val weatherList = listOf("sunny", "cloudy", "rainy")
while (true) {
emit(weatherList.random())
delay(1000)
}
}
fun main() = runBlocking<Unit> {
weatherFlow.collect {
println(it)
} // 将永远挂起
// 这行代码永远不会执行
println("Collection finished")
}
为此,我们应该为每一个 collect 启动一个单独的协程。
所以需要这样:
kotlin
fun main() = runBlocking<Unit> {
launch {
weatherFlow.collect {
println(it)
}
}
println("Collection finished")
}
而 launchIn 可用于让 collect 在指定的 scope 启动的协程中执行,我们来看看这个函数的内部实现:
kotlin
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}
很简单,就是使用了传入的 CoroutineScope 启动了一个新的协程,并在这个协程中调用了 collect()。
我们常常会将它和 onEach() 中间操作符配合使用,下面的代码等价于之前的代码:
kotlin
fun main() = runBlocking<Unit> {
weatherFlow.onEach {
println(it)
}.launchIn(this)
println("Collection finished")
}