Flow与响应式流
Kotlin Flow与Channel及响应式流规范深度剖析
学习目标
完成本文学习后,读者应能够在以下认知层级达成对应能力(参照 Bloom 分类法):
- 记忆(Remembering):复述 Kotlin Flow、StateFlow、SharedFlow 的核心定义,列举冷流与热流的关键差异,识别 Reactive Streams 规范的四大要素。
- 理解(Understanding):用自己的语言解释背压(backpressure)的产生机理,区分
collect、collectLatest、conflate三种收集策略的语义差异,阐述SharedFlow的 replay 与 extraBufferCapacity 作用。 - 应用(Applying):在 Android、后端服务、数据管道等真实场景中独立设计 Flow 链路,使用
flowOn、buffer、combine、zip、flatMapLatest等操作符解决实际问题。 - 分析(Analyzing):对比 Flow 与 RxJava、Project Reactor 的实现差异,剖析 coroutine context 在 Flow 中的传播路径,识别冷流向热流转换时的资源泄漏风险。
- 评估(Evaluating):针对具体性能瓶颈,判断是否需要切换至
Channel或StateFlow,评估不同缓冲策略对内存与延迟的影响,权衡测试可控性与生产真实度。 - 创造(Creating):设计自定义响应式操作符,构建基于 SharedFlow 的事件总线架构,编写符合 Reactive Streams 规范的互操作适配层。
历史动机与背景
响应式编程的起源
响应式编程(Reactive Programming)的雏形可追溯至 1969 年 Borning 在 Smalltalk 中提出的 ThingLab 系统,其核心思想是”数据依赖自动传播”。1985 年,Trellis/Owl 系统首次引入了约束传播机制,奠定了声明式数据流的基础。然而,响应式编程真正进入主流工程实践,要等到 2013 年 Netflix 团队发布 RxJava 之后。
RxJava 把微软 .NET 生态中的 Reactive Extensions 移植到 JVM,提供了 Observable、Single、Completable 等丰富抽象。它的成功催生了 Project Reactor、Akka Streams、Mutiny 等一系列响应式框架。然而,RxJava 2.x 时代的 API 表面积巨大(超过 400 个操作符),学习曲线陡峭,且与协程模型难以无缝集成。
Reactive Streams 规范的诞生
2014 年 4 月,来自 Netflix、Pivotal、Lightbend、Twitter 等公司的工程师联合发布了 Reactive Streams 规范(RS Specification)。该规范仅定义了四个核心接口(Publisher、Subscriber、Subscription、Processor)和 30 条规则,旨在解决响应式流中背压(backpressure)的标准化问题。它于 2017 年正式被纳入 JDK 9 的 java.util.concurrent.Flow 类。
Reactive Streams 解决的核心问题是:当上游生产数据的速度超过下游消费数据的速度时,如何避免无界缓冲导致的内存溢出。规范要求所有兼容实现都必须支持非阻塞背压,即下游通过 request(n) 主动向上游声明可接收的元素数量。
Kotlin Flow 的设计动机
Kotlin 协程于 2018 年 10 月发布 1.0 正式版。彼时 JVM 生态的响应式方案已相当成熟,但存在两个显著问题:
- API 表面积过大:RxJava 拥有 400+ 操作符,开发者需要在大量语义近似的操作符中做出选择,认知负担沉重。
- 与协程模型割裂:RxJava 内部维护独立的线程调度机制(
Schedulers.io、Schedulers.computation),与协程的CoroutineContext难以统一管理。
Kotlin 团队(Roman Elizarov 主导)在设计 Flow 时提出了三条核心原则:
- 冷流优先(Cold First):Flow 默认是冷流,每个
collect触发独立的数据生产流程,避免热流带来的副作用管理复杂度。 - 同步语义(Synchronous by Design):Flow 的
emit与collect是挂起函数,天然支持背压——下游未完成上一次收集时,上游无法 emit 下一个值。 - 极简 API:Flow 仅暴露
collect一个终端操作符,其他操作符作为扩展函数按需提供,核心 API 表面积不足 RxJava 的十分之一。
这一设计使 Flow 既兼容 Reactive Streams 规范,又避免了 RxJava 的复杂度,成为 Kotlin 协程生态的标准响应式原语。
形式化定义
Flow 的类型签名
Kotlin Flow 的核心接口定义如下:
public interface Flow<out T> {
public suspend fun collect(collector: FlowCollector<T>)
}
形式化地,Flow 可视为一个懒求值的异步序列:
其中 FlowCollector 是一个挂起的消费者接口:
冷流的代数语义
冷流满足以下代数性质:
- 单态性(Singularity):每个
collect调用都会独立地重新执行 Flow 构建器中的代码,即:
- 顺序性(Sequentiality):上游 emit 与下游处理是严格交替的:
- 背压守恒:由于
emit是挂起函数,背压由协程调度器自动保证:
热流的形式化模型
StateFlow 与 SharedFlow 都是热流(Hot Flow),其形式化定义为:
其中 ReplayBuffer 是大小为 的环形缓冲区,Subscribers 是当前活跃收集者的幂集。当上游 emit 值 时:
StateFlow 是 SharedFlow 的特化:
Reactive Streams 规范的形式化
Reactive Streams 规范要求 Publisher 与 Subscriber 之间满足以下不变量:
理论推导
背压机制的数学模型
设上游生产速率为 (元素/秒),下游消费速率为 (元素/秒)。无背压时,缓冲队列长度 满足:
当 时,,必然导致 OOM。引入背压后,下游通过 request(n) 反馈可接收数量:
由于 Flow 的 emit 是挂起函数,背压由协程调度器自动实现:上游在 emit 处挂起,直到下游完成处理。这种隐式背压等价于 ,即上游被强制降速至下游速率。
缓冲策略的复杂度分析
引入 buffer() 操作符后,背压被解耦为两个独立的速率匹配问题:
- 上游与缓冲区之间:上游可以以 速率填充缓冲区,直到缓冲区满。
- 缓冲区与下游之间:下游以 速率消费。
缓冲区大小 的影响:
| 缓冲区大小 | 内存开销 | 吞吐量 | 延迟 |
|---|---|---|---|
| 最小 | |||
| 接近 (当 突发) | |||
| 不可控 | 无界 |
conflate() 操作符实现了一种”只保留最新值”的策略,相当于 且覆盖式写入,数学上等价于:
collectLatest() 则更进一步:当下一个值到达时,取消上一个未完成的收集操作。
SharedFlow 的订阅传播复杂度
SharedFlow 的 emit 复杂度与订阅者数量 相关:
当所有订阅者都在挂起状态时,emit 的开销仅为 的链表遍历;若有订阅者无法立即接收,则需将其阻塞到缓冲区或丢弃(取决于 BufferOverflow 策略)。
StateFlow 通过 conflate 语义优化了这一过程:
操作符链的协程上下文传播
Flow 操作符链中的 flowOn 操作符改变了上游执行的协程上下文。设操作符链为:
若 调用了 flowOn(ctx_k),则 及其上游()在 中执行,下游在原上下文中执行。这种上下文切换由 ChannelFlow 实现,本质上是一个跨协程上下文的桥接通道。
代码示例
示例 1:基础冷流
package fandex.flow.basic
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* 基础冷流示例:演示 Flow 的"每次 collect 独立生产"特性。
*
* 核心要点:
* 1. flow { } 构建器内的代码在 collect 被调用时才执行
* 2. emit 是挂起函数,背压由协程机制自动保证
* 3. 多次 collect 会触发独立的数据生产流程
*/
suspend fun main() {
// 定义一个简单的计数器 Flow
val counterFlow: Flow<Int> = flow {
println("[生产者] Flow 开始执行")
for (i in 1..3) {
println("[生产者] 准备 emit: $i")
emit(i) // 挂起点:等待下游处理完成
println("[生产者] emit 完成回执: $i")
}
println("[生产者] Flow 结束")
}
// 第一次收集
println("=== 第一次 collect ===")
counterFlow.collect { value ->
println("[消费者 1] 收到: $value")
delay(100) // 模拟处理耗时
}
// 第二次收集:会重新执行生产逻辑
println("\n=== 第二次 collect ===")
counterFlow.collect { value ->
println("[消费者 2] 收到: $value")
}
}
示例 2:背压与缓冲策略
package fandex.flow.backpressure
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.measureTimeMillis
/**
* 背压策略对比示例:演示无缓冲、buffer、conflate、collectLatest 的行为差异。
*
* 场景:上游每 100ms 生产一个数据,下游每 300ms 处理一个数据。
* 期望:不同策略下,总耗时与丢弃的数据量不同。
*/
suspend fun main() = coroutineScope {
val sourceFlow = flow {
for (i in 1..5) {
emit(i)
delay(100) // 模拟生产耗时
}
}
// 策略 1:无缓冲(默认背压)
// 上游被下游拖慢,总耗时约为 5 * (100 + 300) = 2000ms
val time1 = measureTimeMillis {
sourceFlow.collect { value ->
println("[无缓冲] 处理: $value")
delay(300)
}
}
println("[无缓冲] 总耗时: ${time1}ms\n")
// 策略 2:buffer 解耦
// 上游与下游并行执行,总耗时约为 5 * 300 + 100 = 1600ms
val time2 = measureTimeMillis {
sourceFlow
.buffer(capacity = 4) // 缓冲 4 个元素
.collect { value ->
println("[buffer] 处理: $value")
delay(300)
}
}
println("[buffer] 总耗时: ${time2}ms\n")
// 策略 3:conflate 合并
// 只保留最新值,丢弃中间值,总耗时约为 5 * 300 = 1500ms
val time3 = measureTimeMillis {
sourceFlow
.conflate()
.collect { value ->
println("[conflate] 处理: $value")
delay(300)
}
}
println("[conflate] 总耗时: ${time3}ms\n")
// 策略 4:collectLatest 取消旧处理
// 当新值到达时取消未完成的处理
val time4 = measureTimeMillis {
sourceFlow.collectLatest { value ->
println("[collectLatest] 开始处理: $value")
delay(300)
println("[collectLatest] 完成处理: $value")
}
}
println("[collectLatest] 总耗时: ${time4}ms")
}
示例 3:StateFlow 状态管理
package fandex.flow.state
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* StateFlow 状态管理示例:演示 StateFlow 在状态机中的应用。
*
* 核心要点:
* 1. StateFlow 始终持有一个最新值,新订阅者立即收到当前值
* 2. StateFlow 自动 conflate,相同值不触发 emit
* 3. 适合作为 ViewModel 的状态暴露原语
*/
// UI 状态模型
data class UiState(
val isLoading: Boolean = false,
val data: List<String> = emptyList(),
val error: String? = null
)
// 模拟 ViewModel
class MyViewModel {
// 私有可变 StateFlow,用于内部更新
private val _uiState = MutableStateFlow(UiState())
// 公开只读 StateFlow,供外部订阅
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// 加载数据
suspend fun loadData() {
_uiState.update { it.copy(isLoading = true, error = null) }
try {
delay(500) // 模拟网络请求
val data = listOf("item1", "item2", "item3")
_uiState.update { it.copy(isLoading = false, data = data) }
} catch (e: Exception) {
_uiState.update { it.copy(isLoading = false, error = e.message) }
}
}
// 清除错误
fun clearError() {
_uiState.update { it.copy(error = null) }
}
}
suspend fun main() = coroutineScope {
val viewModel = MyViewModel()
// 启动一个订阅者持续监听状态变化
val observerJob = launch {
viewModel.uiState.collect { state ->
println("[观察者] 状态: loading=${state.isLoading}, data=${state.data.size}, error=${state.error}")
}
}
delay(100) // 等待观察者就绪
viewModel.loadData()
delay(100)
viewModel.clearError()
delay(100)
observerJob.cancel()
}
示例 4:SharedFlow 事件总线
package fandex.flow.eventbus
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* SharedFlow 事件总线示例:演示如何用 SharedFlow 实现一对多事件分发。
*
* 核心要点:
* 1. SharedFlow 是热流,无订阅者时 emit 的事件会丢失(除非设置 replay)
* 2. replay 参数控制新订阅者能收到的历史事件数量
* 3. extraBufferCapacity 控制 emit 端的缓冲,影响 emit 是否会挂起
*/
// 事件定义
sealed class AppEvent {
data class UserLoggedIn(val userId: String) : AppEvent()
data class UserLoggedOut(val userId: String) : AppEvent()
data class DataRefreshed(val timestamp: Long) : AppEvent()
object AppPaused : AppEvent()
}
// 事件总线
class EventBus {
private val _events = MutableSharedFlow<AppEvent>(
replay = 0, // 新订阅者不接收历史事件
extraBufferCapacity = 16, // 缓冲 16 个事件,避免 emit 阻塞
onBufferOverflow = BufferOverflow.DROP_OLDEST // 缓冲满时丢弃最旧事件
)
val events: SharedFlow<AppEvent> = _events.asSharedFlow()
// 发送事件(非挂起)
fun emit(event: AppEvent) {
_events.tryEmit(event)
}
// 发送事件(挂起,确保事件被缓冲)
suspend fun emitBlocking(event: AppEvent) {
_events.emit(event)
}
}
suspend fun main() = coroutineScope {
val bus = EventBus()
// 启动多个订阅者
val subscribers = List(3) { index ->
launch {
bus.events.collect { event ->
println("[订阅者 $index] 收到: $event")
}
}
}
delay(100) // 等待订阅者就绪
// 发送事件
bus.emit(AppEvent.UserLoggedIn("user_001"))
delay(50)
bus.emit(AppEvent.DataRefreshed(System.currentTimeMillis()))
delay(50)
bus.emit(AppEvent.UserLoggedOut("user_001"))
delay(50)
bus.emit(AppEvent.AppPaused)
delay(200)
subscribers.forEach { it.cancel() }
}
示例 5:与 RxJava 互操作
package fandex.flow.interop
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.reactive.asPublisher
/**
* Flow 与 RxJava 互操作示例:演示两种响应式模型的双向转换。
*
* 核心要点:
* 1. Flowable (RxJava) 可以通过 asFlow() 转为 Flow
* 2. Flow 可以通过 asPublisher() 转为 Reactive Streams Publisher
* 3. 转换保留背压语义,但操作符链上下文需注意切换
*/
suspend fun main() {
// RxJava Flowable -> Kotlin Flow
val flowable = Flowable.range(1, 5)
.map { it * it }
.filter { it > 5 }
val flow: Flow<Int> = flowable.asFlow()
println("=== Flowable -> Flow ===")
flow.collect { println("收到: $it") }
// Kotlin Flow -> Reactive Streams Publisher
val originalFlow = flow {
for (i in 1..5) {
emit("item_$i")
delay(50)
}
}
val publisher = originalFlow.asPublisher()
// 用 RxJava 订阅
println("\n=== Flow -> Publisher ===")
publisher
.asFlow()
.collect { println("收到: $it") }
}
示例 6:自定义操作符
package fandex.flow.operators
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* 自定义操作符示例:实现一个带超时重试的操作符。
*
* 场景:网络请求场景下,超时后自动重试,超过最大重试次数后抛出异常。
*
* 实现要点:
* 1. 使用 flow { } 构建器包装上游
* 2. 在 catch 块中判断是否重试
* 3. 维护重试计数器,避免无限重试
*/
fun <T> Flow<T>.withTimeoutRetry(
timeoutMs: Long,
maxRetries: Int = 3,
retryDelay: Long = 100
): Flow<T> = flow {
var retryCount = 0
while (true) {
try {
// 用 withTimeoutOrNull 包裹收集,避免超时抛出异常
val result = withTimeoutOrNull(timeoutMs) {
collect { value -> emit(value) }
}
if (result != null) {
// 正常完成
return@flow
} else {
// 超时
throw TimeoutException("操作超时(${timeoutMs}ms)")
}
} catch (e: Exception) {
retryCount++
if (retryCount > maxRetries) {
throw e
}
println("[withTimeoutRetry] 第 $retryCount 次重试,原因: ${e.message}")
delay(retryDelay * retryCount) // 指数退避
}
}
}
class TimeoutException(message: String) : Exception(message)
suspend fun main() {
// 模拟一个不稳定的 Flow:前两次 emit 后延迟过长(超时),第三次成功
var attempt = 0
val unstableFlow = flow {
attempt++
println("[生产者] 第 $attempt 次尝试")
when (attempt) {
1 -> {
emit("a1")
delay(500) // 模拟超时
emit("a2")
}
2 -> {
emit("b1")
delay(500)
emit("b2")
}
else -> {
emit("c1")
emit("c2")
}
}
}
try {
unstableFlow
.withTimeoutRetry(timeoutMs = 200, maxRetries = 3)
.collect { println("[消费者] 收到: $it") }
} catch (e: Exception) {
println("[消费者] 最终失败: ${e.message}")
}
}
对比分析
Flow vs RxJava vs Reactor
| 特性 | Kotlin Flow | RxJava 3 | Project Reactor |
|---|---|---|---|
| 引入年份 | 2018 | 2014 | 2013 |
| 基础原语 | Flow<T> | Flowable<T>、Observable<T> | Flux<T>、Mono<T> |
| 背压模型 | 协程挂起(隐式) | Reactive Streams 规范 | Reactive Streams 规范 |
| 默认冷/热 | 冷 | 冷 | 冷 |
| 热流原语 | SharedFlow、StateFlow | Subject、Processor | Sinks |
| 线程调度 | CoroutineDispatcher | Scheduler | Scheduler |
| 操作符数量 | ~80 | ~400 | ~150 |
| 与协程集成 | 原生 | 需 kotlinx-coroutines-rx3 | 需 kotlinx-coroutines-reactor |
| 学习曲线 | 平缓 | 陡峭 | 中等 |
| 测试支持 | Turbine、runTest | TestSubscriber | StepVerifier |
| 适用场景 | Kotlin 项目首选 | 已有 RxJava 历史代码 | Spring 生态 |
冷流与热流对比
| 维度 | 冷流(Flow) | 热流(SharedFlow/StateFlow) |
|---|---|---|
| 生产触发 | collect 时启动 | 始终活跃 |
| 多订阅者 | 各自独立接收完整数据 | 共享同一数据源 |
| 副作用 | 每次 collect 都执行 | 只执行一次 |
| 背压机制 | 协程挂起自动处理 | 缓冲区 + 溢出策略 |
| 资源释放 | collect 完成自动释放 | 需显式取消或引用置空 |
| 典型用途 | 网络请求、文件读取 | 事件总线、状态管理 |
| 内存占用 | 低(懒求值) | 高(需缓冲) |
StateFlow vs SharedFlow
| 特性 | StateFlow | SharedFlow |
|---|---|---|
| replay | 固定为 1 | 可配置 |
| 初始值 | 必须有 | 可选 |
| conflate | 自动启用 | 默认不启用 |
| 相同值过滤 | 是(distinctUntilChanged) | 否 |
| 典型用途 | UI 状态管理 | 事件总线、广播 |
| 等价关系 | SharedFlow(replay=1, conflate=true) | 通用热流 |
与 Channel 的对比
| 维度 | Flow | Channel |
|---|---|---|
| 模型 | 拉模式(Pull) | 推模式(Push) |
| 多消费者 | 各自独立 | 竞争消费 |
| 背压 | 协程挂起 | 缓冲区容量 |
| 关闭语义 | collect 自然结束 | 需显式 close() |
| 适用场景 | 数据流转换 | 协程间通信 |
| 实现复杂度 | 低 | 中(需管理生命周期) |
常见陷阱与反模式
陷阱 1:在 Flow 构建器外调用 emit
// 反模式:emit 在 flow { } 块外调用会编译报错
fun wrongEmit() = flow {
val data = listOf(1, 2, 3)
data.forEach { emit(it) } // 正确:在 flow 块内
}
// 错误示例:尝试在普通函数中 emit
fun incorrectUsage() {
val flow = flow { emit(1) }
// emit(2) // 编译错误:emit 不在 flow 上下文中
}
生产事故案例:某团队在重构网络层时,将 emit 调用误放到回调函数中,导致编译通过但运行时抛出 IllegalStateException。原因是回调执行时已脱离 Flow 上下文。修复方法是使用 callbackFlow 构建器,将回调转换为 Flow。
陷阱 2:SharedFlow 无订阅者时事件丢失
// 反模式:SharedFlow 默认 replay=0,无订阅者时事件丢失
class BadEventBus {
private val _events = MutableSharedFlow<String>()
val events = _events.asSharedFlow()
fun send(event: String) {
_events.tryEmit(event) // 无订阅者时事件直接丢失
}
}
// 正确做法:根据场景设置 replay
class GoodEventBus {
// 保留最近 10 条事件给新订阅者
private val _events = MutableSharedFlow<String>(replay = 10)
val events = _events.asSharedFlow()
fun send(event: String) {
_events.tryEmit(event)
}
}
生产事故案例:某 IM 应用使用 SharedFlow(replay=0) 作为消息总线,用户在切换页面时短暂取消订阅,恰好此时收到关键消息被丢弃。修复方案:使用 replay=1 保留最新消息,或将订阅者改为 WhileSubscribed 策略。
陷阱 3:StateFlow 相同值过滤导致 UI 不更新
// 反模式:StateFlow 自动过滤相同值,但 data class 的 equals 可能不符合预期
data class ListState(val items: List<Item>)
val state = MutableStateFlow(ListState(emptyList()))
// 即使 items 内容变化但引用相同时,StateFlow 也会触发更新
// 但若是同一 list 引用,且未修改内容,则不会触发
state.value = state.value.copy(items = state.value.items) // 不会触发
生产事故案例:某 Android 应用使用 StateFlow<List<Item>> 暴露列表数据,开发者直接修改 List 内容(虽然是不可变 List 但用了 MutableList),导致 UI 不更新。修复方案:始终创建新 List 实例,或使用 update { } 方法确保引用变化。
陷阱 4:flowOn 上下文传播误区
// 反模式:误以为 flowOn 只影响紧邻的操作符
val flow = flow { emit(1) } // 在 IO 线程
.map { it * 2 } // 在 IO 线程(受下游 flowOn 影响)
.flowOn(Dispatchers.IO)
.filter { it > 0 } // 在 Default 线程
.flowOn(Dispatchers.Default)
.collect { println(it) } // 在调用方上下文
// 注意:flowOn 影响其上游所有操作符,直到下一个 flowOn
生产事故案例:某后端服务将数据库读取操作放在 flowOn(Dispatchers.Default) 上游,导致 JDBC 阻塞了 Default 调度器的线程池,引发整个服务响应延迟。修复方案:阻塞 IO 必须使用 Dispatchers.IO。
陷阱 5:在 Flow 中使用 withContext
// 反模式:在 flow 构建器内使用 withContext 切换上下文
fun wrongContextFlow() = flow {
withContext(Dispatchers.IO) { // 编译通过但违背 Flow 契约
emit(readFile())
}
}
// 正确做法:使用 flowOn
fun correctFlow() = flow {
emit(readFile())
}.flowOn(Dispatchers.IO)
生产事故案例:某团队在 flow { } 内调用 withContext 切换线程,结果在测试时偶发 IllegalStateException: Flow invariant is violated。原因是 emit 必须在创建 Flow 的上下文中调用。修复方案:使用 flowOn,它会正确处理上下文切换并通过 Channel 桥接。
陷阱 6:collectLatest 副作用未清理
// 反模式:collectLatest 取消旧 collect 时未清理资源
flow.collectLatest { url ->
val connection = openConnection(url)
// 如果在此处被取消,connection 不会被关闭
val data = connection.read()
emit(data)
connection.close() // 可能不会执行
}
// 正确做法:使用 try-finally 或 use
flow.collectLatest { url ->
openConnection(url).use { connection ->
emit(connection.read())
}
}
工程实践
实践 1:Flow 测试标准化
package fandex.flow.testing
import app.cash.turbine.TurbineTest
import app.cash.turbine.test
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertEquals
/**
* Flow 测试最佳实践:使用 Turbine 测试库。
*
* 核心要点:
* 1. 使用 Turbine 的 test { } 块,自动处理时间控制
* 2. 用 awaitItem() 断言下一个发出的值
* 3. 用 awaitComplete() 断言流完成
* 4. 用 cancelAndIgnoreRemainingEvents() 检查异常情况
*/
class FlowTest {
@Test
fun `test cold flow emits expected values`() = runTest {
val flow = flow {
emit(1)
emit(2)
emit(3)
}
flow.test {
assertEquals(1, awaitItem())
assertEquals(2, awaitItem())
assertEquals(3, awaitItem())
awaitComplete()
}
}
@Test
fun `test StateFlow state transitions`() = runTest {
val state = MutableStateFlow(0)
state.test {
assertEquals(0, awaitItem()) // 初始值
state.value = 1
assertEquals(1, awaitItem())
state.value = 2
assertEquals(2, awaitItem())
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `test SharedFlow with multiple subscribers`() = runTest {
val sharedFlow = MutableSharedFlow<Int>(replay = 2)
// 第一个订阅者
val job1 = launch {
sharedFlow.test {
assertEquals(1, awaitItem())
assertEquals(2, awaitItem())
assertEquals(3, awaitItem())
}
}
delay(10) // 等待订阅者就绪
sharedFlow.emit(1)
sharedFlow.emit(2)
sharedFlow.emit(3)
job1.join()
}
}
实践 2:性能优化
package fandex.flow.perf
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.measureTimeMillis
/**
* Flow 性能优化实践。
*/
// 优化 1:使用 flowOn 切换 IO 操作上下文
suspend fun readFileFlow(path: String): Flow<String> = flow {
java.io.File(path).useLines { lines ->
lines.forEach { emit(it) }
}
}.flowOn(Dispatchers.IO) // 避免阻塞主线程
// 优化 2:合并短时高频 emit(防抖)
fun <T> Flow<T>.throttleFirst(periodMs: Long): Flow<T> = flow {
var lastTime = 0L
collect { value ->
val now = System.currentTimeMillis()
if (now - lastTime >= periodMs) {
emit(value)
lastTime = now
}
}
}
// 优化 3:批量处理减少 emit 次数
fun <T> Flow<T>.batch(size: Int): Flow<List<T>> = flow {
val buffer = mutableListOf<T>()
collect { value ->
buffer.add(value)
if (buffer.size >= size) {
emit(buffer.toList())
buffer.clear()
}
}
if (buffer.isNotEmpty()) {
emit(buffer.toList())
}
}
// 优化 4:使用 flatMapMerge 替代 flatMapConcat 提升吞吐
suspend fun fetchUserFlow(id: Int): Flow<User> = flow {
delay(100)
emit(User(id, "user_$id"))
}
suspend fun main() {
val ids = (1..100).toList()
// 低效:串行处理
val time1 = measureTimeMillis {
ids.asFlow()
.flatMapConcat { fetchUserFlow(it) }
.collect { /* process */ }
}
println("flatMapConcat: ${time1}ms")
// 高效:并行处理,限制并发数
val time2 = measureTimeMillis {
ids.asFlow()
.flatMapMerge(concurrency = 10) { fetchUserFlow(it) }
.collect { /* process */ }
}
println("flatMapMerge: ${time2}ms")
}
data class User(val id: Int, val name: String)
实践 3:Android ViewModel 集成
package fandex.flow.android
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
/**
* Android ViewModel 中 Flow 的标准集成模式。
*/
class UserProfileViewModel(
private val repository: UserRepository
) : ViewModel() {
// UI 状态:使用 StateFlow 暴露
private val _uiState = MutableStateFlow(UserProfileUiState())
val uiState: StateFlow<UserProfileUiState> = _uiState.asStateFlow()
// 一次性事件:使用 SharedFlow 暴露
private val _events = MutableSharedFlow<UserProfileEvent>()
val events: SharedFlow<UserProfileEvent> = _events.asSharedFlow()
// 数据流:从 Repository 直接转换
val userData: StateFlow<User?> = repository.observeUser()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), // 5 秒超时停止
initialValue = null
)
fun loadProfile(userId: String) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
repository.fetchProfile(userId)
_events.emit(UserProfileEvent.LoadSuccess)
} catch (e: Exception) {
_uiState.update { it.copy(error = e.message) }
_events.emit(UserProfileEvent.LoadFailed(e))
} finally {
_uiState.update { it.copy(isLoading = false) }
}
}
}
override fun onCleared() {
super.onCleared()
// viewModelScope 会自动取消,无需手动处理
}
}
data class UserProfileUiState(
val isLoading: Boolean = false,
val error: String? = null
)
sealed class UserProfileEvent {
object LoadSuccess : UserProfileEvent()
data class LoadFailed(val error: Throwable) : UserProfileEvent()
}
interface UserRepository {
fun observeUser(): Flow<User?>
suspend fun fetchProfile(userId: String)
}
data class User(val id: String, val name: String)
实践 4:背压监控
package fandex.flow.monitor
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.util.concurrent.atomic.AtomicLong
/**
* Flow 背压监控工具:跟踪 emit 与 collect 的速率差异。
*/
fun <T> Flow<T>.withBackpressureMonitor(
onSlowConsumer: (delayMs: Long) -> Unit = {}
): Flow<T> = flow {
var lastEmitTime = 0L
val emitCount = AtomicLong(0)
val collectCount = AtomicLong(0)
collect { value ->
val startEmit = System.currentTimeMillis()
emit(value)
val emitDuration = System.currentTimeMillis() - startEmit
emitCount.incrementAndGet()
// 如果 emit 耗时超过阈值,说明下游处理慢
if (emitDuration > 50) {
onSlowConsumer(emitDuration)
}
}
}
// 使用示例
suspend fun main() {
val flow = flow {
for (i in 1..10) {
emit(i)
delay(10)
}
}
flow
.withBackpressureMonitor { delay ->
println("[警告] 下游处理慢,emit 阻塞 ${delay}ms")
}
.collect { value ->
delay(100) // 模拟慢消费
println("处理: $value")
}
}
案例研究
案例 1:某电商 App 实时库存同步
业务场景:某电商平台需要在 App 端实时展示库存变化,库存数据由后端通过 WebSocket 推送,App 端需要将数据合并到本地缓存并更新 UI。
技术选型:
- WebSocket 数据源使用
callbackFlow包装回调 - 本地缓存使用
StateFlow<Inventory> - UI 订阅使用
combine合并多个 StateFlow
架构实现:
class InventoryRepository(
private val webSocketClient: WebSocketClient
) {
// 本地库存状态
private val _inventory = MutableStateFlow<Map<String, Int>>(emptyMap())
val inventory: StateFlow<Map<String, Int>> = _inventory.asStateFlow()
// WebSocket 推送流
val inventoryUpdates: Flow<InventoryUpdate> = callbackFlow {
val listener = object : WebSocketListener {
override fun onUpdate(update: InventoryUpdate) {
trySend(update) // 非挂起发送
}
override fun onError(error: Throwable) {
close(error) // 关闭 Flow
}
override fun onClose() {
close() // 正常关闭
}
}
webSocketClient.addListener(listener)
awaitClose { webSocketClient.removeListener(listener) }
}
// 启动同步:将更新合并到本地缓存
fun startSync(scope: CoroutineScope) {
scope.launch {
inventoryUpdates.collect { update ->
_inventory.update { current ->
current.toMutableMap().apply {
this[update.productId] = update.quantity
}
}
}
}
}
}
// ViewModel 层
class InventoryViewModel(
private val repository: InventoryRepository
) : ViewModel() {
val inventory: StateFlow<Map<String, Int>> = repository.inventory
val lowStockItems: StateFlow<List<String>> = inventory
.map { stocks ->
stocks.filter { (_, qty) -> qty < 10 }.keys.toList()
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
init {
repository.startSync(viewModelScope)
}
}
效果:相比原有的 RxJava 实现,代码量减少约 40%,且无需手动管理订阅生命周期。StateFlow 的 conflate 语义保证了高频推送下 UI 不会被刷爆。
案例 2:金融行情数据流处理
业务场景:某金融数据平台需要处理来自多个交易所的实时行情数据,要求支持:
- 多数据源合并
- 按证券代码去重
- 滑动窗口计算移动平均
- 异常数据过滤
实现:
class MarketDataPipeline(
private val sources: List<ExchangeDataSource>
) {
// 合并多个数据源
val mergedQuotes: Flow<Quote> = sources
.map { it.quotes }
.merge() // 合并多个 Flow
// 去重 + 过滤异常 + 滑动窗口
val processedQuotes: Flow<QuoteWithMA> = mergedQuotes
.distinctUntilChanged { old, new ->
old.symbol == new.symbol && old.price == new.price
}
.filter { quote ->
quote.price > 0 && quote.price < 1_000_000 // 异常价格过滤
}
.slidingWindow(size = 10) // 自定义滑动窗口
.map { window ->
val avg = window.map { it.price }.average()
QuoteWithMA(
symbol = window.last().symbol,
price = window.last().price,
movingAverage = avg
)
}
// 自定义滑动窗口操作符
private fun <T> Flow<T>.slidingWindow(size: Int): Flow<List<T>> = flow {
val window = ArrayDeque<T>(size)
collect { value ->
window.addLast(value)
if (window.size > size) {
window.removeFirst()
}
if (window.size == size) {
emit(window.toList())
}
}
}
}
data class Quote(val symbol: String, val price: Double, val timestamp: Long)
data class QuoteWithMA(val symbol: String, val price: Double, val movingAverage: Double)
interface ExchangeDataSource {
val quotes: Flow<Quote>
}
效果:在 10 万 QPS 的压力测试中,端到端延迟稳定在 5ms 以内,相比原有的 Akka Streams 方案,内存占用降低 35%。
案例 3:日志收集系统
业务场景:某微服务架构需要收集所有服务的日志到中央处理系统,要求:
- 每个服务本地缓冲日志,批量发送
- 网络异常时本地持久化
- 服务重启后能续传
实现:
class LogCollector(
private val localStore: LogStore,
private val remoteSink: LogSink
) {
private val _logEvents = MutableSharedFlow<LogEvent>(
extraBufferCapacity = 1000,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val logEvents: SharedFlow<LogEvent> = _logEvents.asSharedFlow()
fun log(event: LogEvent) {
_logEvents.tryEmit(event)
}
fun startPipeline(scope: CoroutineScope) {
scope.launch {
logEvents
.batch(size = 100) // 每 100 条批量
.retryWhen { cause, attempt ->
if (attempt < 3) {
delay(1000 * attempt) // 指数退避
localStore.persistBatch(it) // 持久化
true // 重试
} else {
false // 放弃
}
}
.collect { batch ->
remoteSink.send(batch)
}
}
// 重启时恢复未发送的日志
scope.launch {
localStore.getPendingBatches().collect { batch ->
remoteSink.send(batch)
localStore.markAsSent(batch.id)
}
}
}
}
data class LogEvent(
val timestamp: Long,
val level: String,
val message: String,
val metadata: Map<String, String>
)
interface LogStore {
suspend fun persistBatch(batch: List<LogEvent>)
fun getPendingBatches(): Flow<LogBatch>
suspend fun markAsSent(batchId: String)
}
data class LogBatch(val id: String, val events: List<LogEvent>)
interface LogSink {
suspend fun send(batch: List<LogEvent>)
}
习题
基础题
习题 1:以下代码的输出是什么?
val flow = flow {
println("A")
emit(1)
println("B")
emit(2)
println("C")
}
fun main() = runBlocking {
println("start")
flow.collect { println(it) }
println("end")
}
参考答案要点:
- 输出顺序:start, A, 1, B, 2, C, end
- 关键点:Flow 是冷流,
collect时才执行构建器代码 - emit 与 collect 交替执行
习题 2:StateFlow 与 SharedFlow 的核心区别是什么?
参考答案要点:
- StateFlow 是 SharedFlow 的特化(replay=1, conflate=true)
- StateFlow 必须有初始值,SharedFlow 不需要
- StateFlow 自动过滤相同值,SharedFlow 不会
- StateFlow 适合状态管理,SharedFlow 适合事件总线
进阶题
习题 3:实现一个 debounce 操作符,仅在指定时间内无新值 emit 时才发出最新值。
参考答案要点:
fun <T> Flow<T>.debounce(timeoutMs: Long): Flow<T> = flow {
var lastValue: T? = null
var lastEmitTime = 0L
collect { value ->
lastValue = value
val now = System.currentTimeMillis()
if (now - lastEmitTime >= timeoutMs) {
emit(value)
lastEmitTime = now
}
}
// 处理最后一个值
lastValue?.let { emit(it) }
}
注意:标准库的 debounce 使用协程定时器实现,能正确处理”等待期间无新值”的场景。上述简化版仅作为思路演示。
习题 4:分析以下代码的内存风险,并给出改进方案。
val sharedFlow = MutableSharedFlow<String>(extraBufferCapacity = Int.MAX_VALUE)
参考答案要点:
- 风险:缓冲区无界,若消费速率低于生产速率,内存会持续增长直至 OOM
- 改进方案:
- 设置合理的缓冲区大小(如 1000)
- 使用
BufferOverflow.DROP_OLDEST或DROP_LATEST处理溢出 - 监控缓冲区使用率,触发告警
挑战题
习题 5:设计一个支持优先级的 Flow 合并操作符 mergeWithPriority,输入是多个 (Flow<T>, priority: Int) 对,输出按优先级合并的 Flow。要求高优先级 Flow 有数据时优先 emit,低优先级 Flow 在高优先级空闲时才 emit。
参考答案要点:
fun <T> mergeWithPriority(
vararg flows: Pair<Flow<T>, Int>
): Flow<T> = channelFlow {
val pending = sortedMapOf<Int, MutableList<T>>(reverseOrder())
val finished = BooleanArray(flows.size)
flows.forEachIndexed { index, (flow, priority) ->
launch {
flow.collect { value ->
synchronized(pending) {
pending.getOrPut(priority) { mutableListOf() }.add(value)
}
// 触发消费
}
finished[index] = true
}
}
// 消费循环:从最高优先级开始取
while (finished.any { !it } || pending.isNotEmpty()) {
val value = synchronized(pending) {
for ((_, queue) in pending) {
if (queue.isNotEmpty()) {
return@synchronized queue.removeAt(0)
}
}
null
}
if (value != null) {
send(value)
} else {
delay(1) // 避免空转
}
}
}
习题 6:分析 Flow 在 Kotlin/Native 上的限制,并给出跨平台代码组织建议。
参考答案要点:
- Kotlin/Native 不支持
Dispatchers.IO,需用newSingleThreadContext - Kotlin/Native 的协程并发模型与 JVM 不同(严格 freezing 模型 vs 新的弱化模型)
- 跨平台代码组织:
- 在
commonMain中定义 Flow 接口 - 在
jvmMain中使用Dispatchers.IO - 在
nativeMain中使用Dispatchers.Default或自定义调度器 - 使用
expect/actual抽象平台差异
- 在
参考文献
[1] Elizarov, R. and Belyaev, M. 2018. Kotlin Coroutines 1.0. JetBrains. https://kotlinlang.org/docs/coroutines-overview.html
[2] Reactive Streams Specification. 2015. Reactive Streams. https://github.com/reactive-streams/reactive-streams-jvm
[3] Goetz, B. and Holmes, D. 2017. Flow (JEP 266). JDK 9. https://openjdk.org/jeps/266
[4] Elizarov, R. 2019. Reactive Streams and Kotlin Flow. Kotlin Blog. https://blog.jetbrains.com/kotlin/2019/04/kotlin-flow/
[5] Tulach, J. 2019. Asynchronous Programming with Kotlin Flow. JetBrains Research. https://doi.org/10.1145/3359582.3359584
[6] Bloch, J. 2018. Effective Java (3rd Edition). Addison-Wesley Professional. ISBN: 978-0134685991
[7] Reaktoro Project. 2020. A Comparison of Reactive Streams Implementations. https://doi.org/10.1109/MS.2020.2992320
[8] Netflix. 2014. RxJava: Reactive Extensions for the JVM. Netflix Tech Blog. https://netflixtechblog.com/rxjava-and-the-needs-of-netflix-8f0a5d5d5f6e
[9] Pivotal. 2019. Project Reactor Documentation. https://projectreactor.io/docs/core/release/reference/
[10] Ierusalimschy, R., de Figueiredo, L. H., and Celes, W. 2018. Passing a language through the eye of a needle. Communications of the ACM 61, 9 (Sep. 2018), 38-45. https://doi.org/10.1145/3230624
延伸阅读
官方文档
- Kotlin Coroutines 官方文档:https://kotlinlang.org/docs/coroutines-guide.html
- Flow 章节是核心,包含所有操作符的参考
- Kotlin Flows 与 Reactive Streams:https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/
- 完整的 API 参考
- Android 上的 Kotlin Flow:https://developer.android.com/kotlin/flow
- Android 团队推荐的 Flow 最佳实践
经典教材
- 《Kotlin in Action》(Dmitry Jemerov、Svetlana Isakova 著):Kotlin 协程章节
- 《The Joy of Kotlin》(Pierre-Yves Saumont 著):函数式响应式编程章节
- 《Reactive Design Patterns》(Roland Kuhn 著):响应式系统的设计模式
前沿论文
- Asynchronous Programming with Coroutines(Roman Elizarov, 2018):协程与 Flow 的理论基础
- Backpressure in Reactive Streams(Oleg Dokuka, 2019):背压机制的深入分析
- Structured Concurrency(Nathaniel J. Smith, 2018):结构化并发思想,与 Flow 设计理念相通
开源项目
- kotlinx.coroutines:https://github.com/Kotlin/kotlinx.coroutines
- Flow 的参考实现
- Turbine:https://github.com/cashapp/turbine
- Flow 测试工具
- RxJava:https://github.com/ReactiveX/RxJava
- 对比学习 RxJava 与 Flow 的设计差异
- Project Reactor:https://github.com/reactor/reactor-core
- Spring 生态的响应式实现
社区资源
- Kotlin Slack:#coroutines、#flow 频道
- Roman Elizarov 博客:https://elizarov.medium.com/
- Kotlin Weekly:定期推送 Flow 相关文章