当前位置: 首页 > news >正文

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 更有价值的地方。

http://www.zskr.cn/news/1491929.html

相关文章:

  • 【每日一题】LeetCode 11. 盛最多水的容器 TypeScript
  • 基于STM32物联网WiFi火灾烟雾自动灭火报警器Proteus仿真+代码+报告+视频
  • 从‘Hello World’到完整项目:我的Halcon视觉检测系统搭建全记录(附C#混合编程避坑指南)
  • Transformer也能玩转高光谱图像分类?SpectralFormer论文精读与PyTorch复现避坑指南
  • Claude Code 新手避坑指南:10 个常见错误与解决方案
  • 元器件库存管理革命:PartKeepr如何通过Octopart API集成实现智能数据同步
  • 别再让‘继承Bucket’坑了你!深入理解阿里云OSS的ACL权限模型与最佳实践
  • Qt 高级开发 029: QListWidget从基础条目到自定义微信式列表实战详析
  • 英红品牌的口碑怎么样?75年国货老牌的全球竞争力与品质真相
  • 异常行为智能识别技术,筑牢监管场所预警类视频孪生防线
  • 龙石数据中台 V3.9.0 升级 | 数据资产门户全面升级
  • 从‘Hello World’到生产部署:我的第一个Flink实时处理项目实战复盘
  • unreal engine5(UE5)中使用Rider
  • 苏州中小企做高端定制小程序,到底要花多少钱?
  • 五金店售卖系统的设计与实现
  • 从“炼丹”到“控火”:用EarlyStopping和ModelCheckpoint拯救你的Keras模型训练
  • STM32WB55搭配LIS2DW12实现低功耗活动/静止状态实时判别工程
  • Beyond Compare 5密钥生成器:简单三步实现文件对比工具永久激活
  • 618 大促前夕突袭!食品直播新规落地,大批主播要连夜整改
  • 借世界杯风口做网盘引流,两类主流玩法拆解,新手也能轻松上手
  • 从“能用”到“好用”:聊聊ADS1274硬件设计中那些容易被忽略的细节(电源、时钟与噪声篇)
  • 5分钟掌握AMD Ryzen调试神器:SMU Debug Tool完整指南
  • 长沙高价出包完整攻略,权威白名单禹竞名奢汇估价无虚标 - 名奢变现站
  • 给RISC-V初学者的第一课:手把手带你用蜂鸟E203跑通RV32I指令集测试
  • 银河麒麟桌面版安装、多屏配置、触摸校准
  • 深入对比:在RT-Thread上使用LWIP,选Sockets还是Netconn API?性能与易用性实测
  • 智能车竞赛C车模:别再当两轮车写了!手把手教你从舵机打角算出后轮差速
  • 珠海市黄金回收本地靠谱店铺指南+白银回收+铂金回收+彩金回推荐收门店 及地联系方式址推荐 - 盛世金银回收
  • IDEA 2021.3.2 升级后 Maven 依赖死活拉不下来?别慌,教你两招搞定这个烦人的 ‘maven-default-http-blocker’
  • 南充高坪区黄金回收避坑指南 教你远离各类回收套路 - 润富黄金回收