详解Flow的collectLatest { ... }

它到底做了什么?

  • 对上游每一次 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(),即可稳稳拿到"只处理最新"的高效流水线。

相关推荐
南北是北北2 小时前
Flow中的buffer详解
面试
小高0072 小时前
🤔函数柯里化:化繁为简的艺术与实践
前端·javascript·面试
Sherry0072 小时前
【译】掌握 Flexbox 的终极指南:从烤肉串到鸡尾酒香肠的布局哲学
css·面试·flexbox
绝无仅有3 小时前
大厂Redis高级面试题与答案
后端·面试·github
绝无仅有3 小时前
面试问题之导致 SQL 查询慢的原因及优化建议
后端·面试·github
在未来等你5 小时前
Kafka面试精讲 Day 18:磁盘IO与网络优化
大数据·分布式·面试·kafka·消息队列
程序员清风5 小时前
滴滴三面:ZGC垃圾收集器了解吗?
java·后端·面试
南北是北北5 小时前
Flow中的背压与并发
面试
洋楼街的奇妙圆子5 小时前
学术论文检索聚合 MCP 服务
面试·mcp