Flow执行模型:上下文保持与生产消费节奏

一条Flow链不仅描述数据如何转换,也隐含两组运行约束:每个操作符在哪个协程上下文中执行,以及生产速度超过消费速度时如何处理积压数据。

flowOn解决上游执行位置,buffer、conflate和collectLatest解决生产消费节奏。它们都可能改变运行过程,却不应改变业务语义。上下文应服从任务类型,背压策略则必须服从每条数据是否允许跳过、旧处理是否已经失效。

内容摘要

  1. Flow遵循上下文保持原则,下游默认运行在收集协程的上下文中。
  2. flowOn只影响它前面的上游,不会把后面的collect一起切走。
  3. 多个flowOn可以把Flow链划分为不同执行区段。
  4. buffer保留数据并解耦生产消费,缓冲区满后上游仍会挂起。
  5. conflate跳过积压的中间值,collectLatest还会取消旧值的处理代码块。

一、上下文保持让下游位置可预测

scss 复制代码
viewModelScope.launch {
    // 下游默认继承启动收集的 viewModelScope 上下文
    repository.users()
        // 没有 flowOn 边界时,转换与 collect 使用同一收集上下文
        .map(::toUiModels)
        .collect(::render)
}

collect使用启动收集的协程上下文。这里由viewModelScope启动,通常处于Dispatchers.Main.immediate,因此collect和render在Main上下文中执行。

collect本身并不固定属于主线程:

scss 复制代码
scope.launch(Dispatchers.Default) {
    // collect 并不固定属于 Main,这里会在 Default 上下文消费数据
    flow.collect(::calculate)
}

此时下游就在Default上下文中。准确关系是:

sql 复制代码
收集协程的Context
→ 决定下游操作符和collect的默认执行上下文

普通flow {}还要求发射遵守上下文保持。下面的写法不应使用:

scss 复制代码
flow {
    withContext(Dispatchers.IO) {
        // 错误示例:普通 flow 不允许切换上下文后再 emit
        emit(api.loadData())
    }
}

withContext改变了执行emit的上下文,破坏普通Flow对发射上下文的一致约束。Flow上游切换应由flowOn表达。

本节小结

Flow不会把整条链固定到某个线程。收集协程决定下游上下文,普通Flow保持发射上下文一致,从而让调用方可以预测数据最终在哪里被消费。

二、flowOn把上游划分为执行区段

scss 复制代码
viewModelScope.launch {
    flow {
        // 数据源位于第一个 flowOn 的上游区段
        emit(api.loadData())
    }
        .map(::parseData)
        // 数据源和 parseData 在 IO 上下文执行
        .flowOn(Dispatchers.IO)
        .map(::calculateResult)
        // calculateResult 位于新的 Default 上游区段
        .flowOn(Dispatchers.Default)
        // collect 不受前方 flowOn 影响,仍使用收集协程上下文
        .collect(::render)
}

判断范围时,从每个flowOn向前看,直到遇到另一个flowOn边界:

rust 复制代码
api.loadData、parseData → Dispatchers.IO
calculateResult         → Dispatchers.Default
collect、render         → 收集协程Context

flowOn在需要时会在上下游之间引入协程和通道边界,让不同区段在各自上下文中协作。它影响的是前面的上游,不会逆向改变后面的下游。

Dispatcher应匹配任务性质

css 复制代码
阻塞式网络、文件、数据库调用 → IO
CPU密集解析、排序、计算      → Default
UI渲染                     → Main

调度器不是越多越好。频繁切分区段会增加调度和数据传递成本,也会让链路更难阅读。若数据源本身已经保证在合适上下文执行,调用层无需重复添加flowOn。

flowOn接收的是CoroutineContext,因此也可以附加调试名称:

less 复制代码
.flowOn(
    // flowOn 可以组合 Dispatcher 与调试名称等 Context 元素
    Dispatchers.IO + CoroutineName("LoadUsers")
)

withContext与flowOn的职责边界

css 复制代码
普通挂起函数中的局部步骤 → withContext
Flow前方的一段上游链路  → flowOn

二者都可能改变Dispatcher,但表达的结构不同。withContext执行当前协程中的顺序代码块,flowOn为Flow操作符链建立上游上下文边界。

本节小结

flowOn只改变前方上游,并可通过多个边界形成执行区段。区段划分应反映IO、计算和UI任务的真实性质,不能把调度器当作性能装饰。

三、默认Flow如何协调生产与消费

scss 复制代码
flow {
    repeat(3) { value ->
        // 默认情况下,emit 会等待下游处理当前值后再继续
        emit(value)
    }
}.collect { value ->
    // 下游耗时会直接约束上游的生产速度
    process(value)
}

默认情况下,emit需要等待下游完成当前值的处理后才能继续,因此执行节奏近似:

复制代码
生产1 → 处理1 → 生产2 → 处理2 → 生产3 → 处理3

这种顺序传播提供天然背压:下游变慢,上游也会被约束,不会无限制地产生待处理数据。但它也意味着生产和消费耗时直接相加,无法重叠执行。

加入buffer后,上游和下游可以在不同协程中重叠运行:

scss 复制代码
records()
    // 有限缓冲让生产与消费重叠;缓冲满后上游仍会挂起
    .buffer(capacity = 64)
    // 每条记录都保留并最终交给持久化逻辑
    .collect(::persist)
复制代码
上游继续生产 → 数据进入缓冲区
下游同时消费 → 从缓冲区取数据
缓冲区已满   → 上游再次挂起

buffer原则上保留每条数据,但它不是无限仓库。容量会影响内存占用、生产阻塞时机和突发流量承受能力。

本节小结

默认Flow通过挂起上游形成背压,buffer通过有限缓存解耦生产和消费。数据必须完整处理时,可以调整吞吐和容量,但不能用丢值换速度。

四、只保留最新值有两种不同语义

conflate跳过等待中的中间值

scss 复制代码
downloadProgress()
    // 下游忙碌时只保留最新待处理进度,不取消当前渲染
    .conflate()
    .collect(::renderProgress)

当下游正在处理旧值时,新值持续到达,conflate只保留最新的待处理值:

css 复制代码
正在处理值A
值B、C、D陆续到达
→ A继续处理
→ B、C被替换
→ A完成后处理D

它适合进度、坐标、传感器快照等只关心近期状态的数据。交易记录、日志和操作命令等不可丢失数据不适合合并。

collectLatest取消旧处理

scss 复制代码
queryFlow.collectLatest { query ->
    // 新关键词到达时,旧关键词对应的代码块会收到取消信号
    val result = api.search(query)

    // 只有仍然有效的最新请求结果才继续渲染
    render(result)
}

新值到来时,旧值对应的收集代码块会被取消:

css 复制代码
开始处理A
值B到达
→ 取消A的处理
→ 开始处理B

这适合搜索联想、页面切换后的内容加载等"新输入使旧任务失效"的场景。

取消仍然是协作式的。网络挂起函数、delay等可取消挂起点可以及时响应;没有挂起点的长计算需要调用ensureActive()、检查isActive或适当yield()。否则旧计算可能在收到取消信号后仍继续占用线程。

三种策略的业务判断

业务要求 操作符 运行结果
每条数据都必须处理 buffer 暂存并完整消费
只需要较新的状态 conflate 当前处理完成,跳过积压中间值
新值使旧处理失效 collectLatest 取消旧处理,立即处理新值

本节小结

conflate丢弃等待中的旧值,不取消正在执行的处理;collectLatest连旧处理也会取消。两者的差异是业务有效期,不只是性能表现。

结语:执行优化不能改变数据含义

Flow运行模型可以分成两个坐标:

arduino 复制代码
在哪里执行
→ 收集Context + flowOn区段

数据来得太快怎么办
→ buffer / conflate / collectLatest

上下文选择决定资源匹配,节奏策略决定数据完整性和处理有效期。任何优化都应先证明不会改变业务结果,再讨论线程利用率、响应速度和内存容量。

参考资料

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui