详解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(),即可稳稳拿到"只处理最新"的高效流水线。

相关推荐
林希_Rachel_傻希希29 分钟前
web性能优化之延迟加载图片和<inframe>
前端·javascript·面试
帅次34 分钟前
Android 高级工程师面试:Java 多线程与并发 近1年高频追问 22 题
android·java·面试
林希_Rachel_傻希希2 小时前
web性能优化之——AI总结视频
前端·javascript·面试
子建莫敌2 小时前
ROS2 面试总结
面试·职场和发展
前端炒粉2 小时前
个人简历面经总结二
前端·网络·vue.js·react.js·面试
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第154题】【06_Spring篇】第14题:Spring 支持的 Bean 作用域
java·开发语言·spring·面试
小林ixn2 小时前
用 100 行代码手搓一个 MCP Server,让 LLM 直接读你本地文件
面试·llm
wear工程师3 小时前
可重复读能不能防幻读?MVCC 和 Next-Key Lock 到底谁在起作用
mysql·面试
weedsfly3 小时前
Cookie 安全三属性:HttpOnly、Secure、SameSite 分别防什么?
前端·javascript·面试
多年小白3 小时前
第八篇 模拟面试套卷
人工智能·ai·面试·职场和发展