它到底做了什么?
- 对上游每一次 emit(value),都会启动一次 收集体(你的 { ... } 块) 。
- 如果下一个值在前一个收集体尚未完成时到来 ,则取消 前一个收集体,并立刻 用最新值启动新的收集体。
- 顺序仍按到达顺序,不会乱序;只是前一个处理"被中断"。
rust
flow
.collectLatest { value ->
// 处理 value(可能很慢)
// 若期间来了新值,则这里会被取消(抛 CancellationException)
}
用一句话: "只让最新的处理跑到头;旧的处理中途就地结束。"****
取消机制(cooperative cancellation)
-
取消是协作式 :只有在挂起点(suspension points)才能"感知"到取消。
-
你的 { ... } 内部如果是纯 CPU 密集 且没有挂起点,会取消不及时。
-
典型做法:
- 让重活封装在 挂起函数(含 delay()、IO 等)里;
- 或在循环中显式检查 coroutineContext.isActive / 调用 yield()。
scss
collectLatest { v ->
heavySteps(v) // 挂起函数,内部含若干挂起点 → 可被及时取消
}
suspend fun heavySteps(v: V) {
step1() // 挂起点
if (!currentCoroutineContext().isActive) return
step2()
yield() // 给调度器一个取消"切入点"
step3()
}
清理善后:被取消时会抛 CancellationException,可用 try/finally 做清理;必要时用 withContext(NonCancellable) 包住清理段。
scss
collectLatest { v ->
try {
render(v) // 可能很慢
} finally {
withContext(NonCancellable) { releaseGL() }
}
}
和conflate()/buffer()的核心区别
- conflate():跳过中间输入 ,但不取消正在执行的下游处理------下游会把手头这次做完,只是下一次拿到的是"最新值"。
- collectLatest:来了新值就取消当前处理,转去处理最新值。
- buffer():只是插个队列,不丢不取消(默认 SUSPEND),主要解耦上下游背压。
诉求 | 选谁 |
---|---|
"只关心最新值,且当前任务可以被打断(重计算/渲染/网络请求可取消)" | collectLatest / mapLatest |
"只关心最新值,但当前任务必须做完(不愿中途取消)" | conflate() 或 buffer(1, DROP_OLDEST) |
"不能丢任何值" | buffer(n, SUSPEND)(或更严格的可靠通道/持久化) |
放置位置(非常关键)
把 collectLatest 放在"重活/可中断的副作用"那一侧,让新值能中断旧处理:
scss
// ✅ 正例:输入频繁、下游渲染/请求很慢 → 新值能打断旧渲染
queryFlow
.debounce(250)
.distinctUntilChanged()
.flatMapLatest(repo::search) // 上游变体:变换侧用 *Latest* 更彻底
.collectLatest { result -> // 下游副作用也用 *Latest*,新结果打断旧渲染
renderResult(result) // 可中断
}
反例 :把所有重活放在 collectLatest 之前(比如在 map { slow() }),那段重活仍会对每个输入执行 ,collectLatest 放再后面也救不了。
与flowOn的关系
- flowOn(Dispatchers.IO) 在切换处自带缓冲边界;collectLatest 在下游 取消自己的处理,不会直接取消上游正在 IO 里跑的那段已开始的运算。
- 如果希望上游重计算也随最新输入被中断 ,在变换侧 用 mapLatest/flatMapLatest/transformLatest,并把重活放到这些 Latest 操作符内部(且该重活要"可取消")。
常见范式
1) 搜索联想(输入 → 网络 → UI)
scss
textChanges()
.debounce(250)
.distinctUntilChanged()
.flatMapLatest { q -> repo.search(q) } // 新查询取消旧查询
.collectLatest { list -> // 新结果到来取消旧渲染
showSuggestions(list)
}
2) 图片加载(只显示最新 URL)
scss
imageUrlFlow()
.distinctUntilChanged()
.mapLatest { url -> // 新 URL 取消旧下载
imageLoader.load(url) // 挂起下载,可取消
}
.collectLatest { bmp -> // 新图到来取消旧的过渡动画/渲染
imageView.setImageBitmap(bmp)
}
3) 进度渲染(频繁帧 → 只渲最新帧)
scss
progressFlow() // 高频 0..100
.collectLatest { p -> // 新进度到来,取消旧一次绘制
drawProgress(p) // 确保可取消(挂起点 or yield)
}
错误与异常处理
- collectLatest 里的取消会抛 CancellationException,不应当作错误上报;一般不在 catch 里吞掉它。
- 如果需要对上游错误 做处理,把 catch { ... } 放在 collectLatest 之前:
scss
flow
.mapLatest { risky() } // 这里抛的 Exception 会被下面 catch 捕捉
.catch { e -> showError(e) }
.collectLatest { render(it) }
性能与背压小贴士
- collectLatest 不等于限速 :它只是取消旧处理,并不会自动让上游"慢下来"。如果上游喷得过快、且你还想减小总工作量,配合 debounce() / sample() / conflate() 使用。
- CPU 密集段请加入协作取消点,否则"取消不及时"会被误解为"collectLatest 不生效"。
和"家族成员"的定位
- mapLatest / flatMapLatest / transformLatest:把"可中断 "的重计算 放在变换侧(更早、更省工)。
- collectLatest:把"可中断 "的副作用/渲染/落盘 放在终端侧。
- 常见组合:上游 flatMapLatest + 下游 collectLatest,双层"最新优先"。
一句话总结
collectLatest :新值一来就取消 当前收集体,转而处理最新值 。把它放在慢且可中断的副作用处,配合 mapLatest/flatMapLatest 与 debounce()/conflate(),即可稳稳拿到"只处理最新"的高效流水线。