Kotlin 协程设计思想(九):Flow 到底是什么?为什么 suspend 函数还需要 Flow?

------ 从 suspend、Sequence 到 Cold Flow,彻底讲透 Kotlin Flow 的设计哲学

前面几篇,我们已经讲了:

CoroutineContext

Job

Dispatcher

launch / async

Exception

Structured Concurrency

Supervisor

suspend

到第八篇,我们已经知道:

suspend 不是开启协程。

它真正解决的是:

复制代码
协程如何暂停
协程如何恢复

suspend 的底层核心是:

复制代码
Continuation + 编译器状态机

到这里,很多人会觉得:

既然已经有了 suspend,为什么还要有 Flow

这就是第九篇真正要讲的问题。

不是:

复制代码
Flow 怎么用?

而是:

复制代码
为什么 Kotlin 已经有 suspend,还要设计 Flow?

一、前面其实埋了一个坑

前面我们一直在讲:

复制代码
suspend fun login() {

}

它的特点是什么?

一句话:

复制代码
一次调用,一个结果

例如:

复制代码
suspend fun getUser(): User

执行流程是:

复制代码
请求用户信息
↓
返回 User
↓
结束

这非常适合接口请求。

例如:

复制代码
val user = getUser()

调用一次,返回一个结果,然后结束。

但是问题来了。

如果是定位呢?

复制代码
1 秒返回一次经纬度

如果是 WebSocket 呢?

复制代码
不断收到服务端消息

如果是蓝牙通知呢?

复制代码
设备一直上报数据

如果是下载进度呢?

复制代码
1%、2%、3%、4%...

这些场景都有一个共同特点:

复制代码
不是一次结果
而是连续结果

这时候,单纯的 suspend fun 就不够用了。


二、suspend 最大的限制

suspend 最大的特点是:

复制代码
一次调用,一个结果

例如:

复制代码
val user = getUser()

这很好理解。

但是现实世界里,很多数据不是一次性的。

而是源源不断的。

例如:

复制代码
定位
WebSocket
蓝牙通知
聊天消息
股票价格
传感器数据
下载进度
数据库变化

这些数据不是:

复制代码
请求一次
返回一次
结束

而是:

复制代码
不断产生
不断更新
不断消费

如果只靠 suspend fun,就会很尴尬。

因为 suspend fun 只能这样:

复制代码
suspend fun getLocation(): Location

它只能返回一个 Location

但是定位真正需要的是:

复制代码
Location
Location
Location
Location
...

所以:

suspend 解决的是一次结果。

而:

Flow 解决的是连续结果。


三、其实 Java 早就遇到过这个问题

在 Java 里,如果我们想一个个拿数据,会用什么?

复制代码
Iterator

例如:

复制代码
while (iterator.hasNext()) {
    Object item = iterator.next();
}

它的特点是:

复制代码
一次拿一个
不断往后拿

后来 Kotlin 里有了:

复制代码
Sequence

例如:

复制代码
val sequence = sequence {
    yield(1)
    yield(2)
    yield(3)
}

它也是一个个产生数据。

使用时:

复制代码
sequence.forEach {
    println(it)
}

输出:

复制代码
1
2
3

Sequence 的特点是:

复制代码
懒加载
按需产生
一个个消费

这和 Flow 很像。

但是 Sequence 有一个问题:

它不支持挂起。


四、Sequence 为什么不够?

例如:

复制代码
val sequence = sequence {
    yield(1)
    delay(1000)
    yield(2)
}

这段代码是不成立的。

因为 sequence {} 里面不能直接调用挂起函数。

也就是说,Sequence 可以做到:

复制代码
一个个产生数据

但是它做不到:

复制代码
一边等待
一边挂起
一边继续产生数据

而现实中的连续数据,往往都需要等待。

例如:

复制代码
1 秒后产生一次定位
网络消息来了才产生一次数据
蓝牙通知来了才产生一次数据
数据库变化了才产生一次数据

这些都不是普通的同步迭代能解决的。

所以可以这样理解:

复制代码
Sequence = 同步数据序列
Flow = 支持挂起的异步数据序列

五、突然理解 Flow

很多教程会说:

复制代码
Flow 是数据流

这句话没错,但是太抽象。

我觉得更适合初学者理解的一句话是:

复制代码
Flow = 支持 suspend 的 Sequence

Sequence 是:

复制代码
一个个给数据

Flow 也是:

复制代码
一个个给数据

但 Flow 比 Sequence 多了一个能力:

复制代码
每次给数据之前,可以挂起

例如:

复制代码
val flow = flow {
    emit(1)

    delay(1000)

    emit(2)

    delay(1000)

    emit(3)
}

这段代码表达的意思是:

复制代码
先发 1
等 1 秒
再发 2
再等 1 秒
再发 3

这个能力,Sequence 做不到。

Flow 能做到,是因为:

复制代码
emit 是 suspend
collect 也是 suspend

六、emit 到底是什么?

例如:

复制代码
flow {
    emit(1)
    emit(2)
    emit(3)
}

很多人觉得:

复制代码
emit 就是发送数据

这句话没错,但还不够。

更准确地说:

复制代码
emit 是一次可能挂起的数据发送

为什么 emit() 也要是 suspend

因为生产者和消费者之间可能存在速度差。

例如:

复制代码
生产者很快
消费者很慢

生产者不停发:

复制代码
1
2
3
4
5

但是消费者处理不过来。

这时候怎么办?

不能无限制乱发。

所以 emit() 必须有能力:

复制代码
等待消费者处理
必要时挂起自己

这就是为什么:

复制代码
emit()

本身也是 suspend


七、collect 为什么也是 suspend?

再看:

复制代码
flow.collect {
    println(it)
}

为什么 collect() 也是 suspend

因为 collect 的本质是:

复制代码
等待数据
处理数据
继续等待
继续处理

尤其是很多 Flow 可能永远不会结束。

例如:

复制代码
WebSocket 消息流
定位数据流
蓝牙通知流
数据库监听流

它们的特点是:

复制代码
只要页面还在
就一直等待数据

所以 collect 必须能够挂起当前协程。

否则它就只能阻塞线程。

所以:

复制代码
collect 是 suspend

本质上是因为:

复制代码
它要等待连续数据

八、为什么 Flow 是 Cold?

这是 Flow 最经典的问题。

例如:

复制代码
val flow = flow {
    println("开始")
    emit(1)
}

这里会不会马上打印:

复制代码
开始

答案是:

不会。

因为只是创建了一个 Flow 对象。

此时:

复制代码
没人 collect
就没人生产

只有真正调用:

复制代码
flow.collect {
    println(it)
}

才会开始执行 flow {} 里面的代码。

所以 Flow 默认是 Cold Flow。

所谓 Cold Flow,就是:

复制代码
没人收集
就不生产数据

九、为什么要设计成 Cold?

这个设计非常重要。

例如在 Android 页面里:

复制代码
页面进入
开始 collect

页面退出
停止 collect

当没人 collect 的时候,Flow 就不再生产数据。

这样有几个好处:

复制代码
节约资源
避免无意义任务
配合生命周期更安全
减少内存泄漏

比如定位。

如果页面已经退出了,还在后台一直生产定位数据,就很浪费。

Cold Flow 的设计正好解决这个问题:

复制代码
需要时才开始
不需要时就停止

这和协程的结构化并发思想是统一的。


十、突然和前面串起来了

现在我们回头看整个体系。

复制代码
suspend:一次调用,一个结果

Flow:一次调用,多个结果

Channel:消息队列

callbackFlow:把回调转成 Flow

StateFlow:状态流

SharedFlow:事件流

这样一看,很多东西就不再是零散 API 了。

它们分别解决不同的问题。

suspend 解决:

复制代码
一次异步结果

Flow 解决:

复制代码
连续异步结果

Channel 解决:

复制代码
协程之间排队传消息

callbackFlow 解决:

复制代码
把传统 callback 接入协程世界

StateFlow 解决:

复制代码
状态保存和状态观察

SharedFlow 解决:

复制代码
事件分发和一次性通知

这才是 Flow 真正的位置。


十一、Google 为什么还要设计 Flow?

因为现实业务里,数据形态不止一种。

有些数据是一次性的。

例如:

复制代码
登录
获取用户信息
提交表单
上传结果

这些适合:

复制代码
suspend fun

有些数据是连续的。

例如:

复制代码
定位
下载进度
WebSocket
蓝牙通知
数据库监听
页面状态变化

这些适合:

复制代码
Flow

所以 Kotlin 协程体系不是简单地"多造了几个 API"。

它是在补完整个异步编程模型:

复制代码
一次结果
连续结果
状态
事件
消息
回调

十二、整个协程体系的设计主线

现在再看这些概念:

复制代码
Thread
Coroutine
suspend
Flow
StateFlow
SharedFlow
Channel
callbackFlow

它们其实一直在回答不同层次的问题。

复制代码
Thread:任务怎么执行?

Coroutine:任务怎么暂停和恢复?

suspend:一次异步结果怎么表达?

Flow:连续异步结果怎么表达?

StateFlow:状态怎么保存和观察?

SharedFlow:事件怎么分发?

Channel:消息怎么排队?

callbackFlow:回调怎么接入协程体系?

你会发现:

Kotlin 协程体系,并不是一堆零散 API。

它真正解决的是:

程序里的任务和数据,应该如何流动。


十三、最终总结

如果让我一句话解释 suspend,我会说:

复制代码
suspend 解决一次异步结果。

如果让我一句话解释 Flow,我会说:

复制代码
Flow 是支持 suspend 的 Sequence。

如果让我解释为什么已经有了 suspend,还要有 Flow,我会说:

复制代码
suspend 只能表达一次结果。
Flow 可以表达连续结果。

所以:

复制代码
suspend fun getUser(): User

适合一次请求。

而:

复制代码
fun observeLocation(): Flow<Location>

适合连续数据。

真正理解 Flow,不是记住:

复制代码
emit
collect
flowOn
StateFlow
SharedFlow

而是理解:

复制代码
现实世界里有大量连续产生的数据。
suspend 只能解决一次结果。
Flow 才能解决连续结果。

这就是 Flow 的设计哲学。


下篇预告

到这里,整个协程设计思想系列已经走到了一个非常有意思的位置。

我们已经讲完了:

复制代码
CoroutineContext
Job
Dispatcher
launch / async
Exception
Structured Concurrency
Supervisor
suspend
Flow

那么最后一个问题来了:

复制代码
Kotlin 协程到底解决了什么问题?

下一篇继续:

Kotlin 协程设计思想(十):Kotlin 协程到底解决了什么问题?

------ 从 Thread、Future、Callback、RxJava 到 Coroutine、Flow,彻底讲透 Kotlin 协程的发展脉络,以及 Google 为什么最终选择了这套设计。

最后再补一句。

整个《Kotlin 协程设计思想》系列,其实一直在回答同一个问题:

复制代码
程序里的任务和数据,到底应该如何流动?

Thread 解决怎么执行。

Coroutine 解决怎么暂停。

suspend 解决一次异步结果。

Flow 解决连续异步结果。

StateFlow 解决状态。

SharedFlow 解决事件。

Channel 解决消息队列。

callbackFlow 解决回调接入。

这才是这个系列真正的主线。

也正是它比单纯讲 API 更有价值的地方。

相关推荐
wardenlzr10 小时前
车机看门狗(Watchdog)为何会让整机硬重启
android·优化·车机
IT毕设实战小研11 小时前
基于大数据的商场商铺数据分析与可视化的设计与实现
android·java·大数据·django·课程设计
aqi0011 小时前
一文读懂 HarmonyOS 7.0 带来的十大API重要升级
android·华为·harmonyos·鸿蒙·harmony
是店小二呀12 小时前
鸿蒙PC开源移植:CodeLite原生IDE与Remote Agent适配
android·智能手机·远程桌面
不要喷香水12 小时前
00-总览与架构
kotlin·app·档案管理
终端安全笔记13 小时前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
爱笑鱼13 小时前
Android 系统启动机制(八):系统服务怎样从创建走到可用?SystemServiceManager 和 Boot Phase 各管什么?
android
hai_android13 小时前
Android 组件化开发实践
android·java·kotlin
mmsx13 小时前
Android 几何构造器的双栈机:链式 API 底层是怎么装配几何的
android