前面几节把 Flow 的操作符、状态流、生命周期都讲完了,这一节做一次横向对照:Flow 和 RxJava 到底什么关系、能不能互相替代、迁移时哪些地方会踩坑。面试里这题很常见,因为它考的是"你是否理解两者的设计取向",而不是 API 记忆。
先把结论放在前面:两者解决的问题高度重叠,但设计取向相反。RxJava 是"响应式流 + 背压策略"体系,核心对象是 Flowable(支持背压)与 Observable(不支持);Flow 是"协程 + 挂起函数"体系,靠 emit 天然挂起实现背压,没有 BackpressureStrategy 枚举。结论有三层:①大部分场景 Flow 可以替代 Rx(网络流、状态流、UI 事件);②Rx 在少数场景仍有优势(observeOn 精确到下游、成熟的算子生态、精细的调度控制);③最需要小心的是 Subject/Subject 语义——这是迁移时最容易出错的地方。
机制背后的执行路径
先看背压机制的根本差别。RxJava 的 Flowable 需要显式指定策略:
Flowable.interval(0, 100, TimeUnit.MILLISECONDS)
.onBackpressureBuffer() // 缓冲
.onBackpressureDrop() // 丢弃
.onBackpressureLatest() // 保留最新
.observeOn(AndroidSchedulers.mainThread());
Flow 没有这些策略,因为 emit 是挂起函数:下游处理慢时上游发射者被挂起,反压自然形成。这是 Kotlin 的语言优势——用挂起替代了协议。代价是 RxJava 里那些"用策略表达不同业务意图"的写法,在 Flow 里要改用 buffer、conflate、flowOn 组合表达。
再看最关键的对照:Subject vs SharedFlow/StateFlow。
| RxJava | Kotlin | 语义差别 | |---|---|---| | BehaviorSubject | StateFlow | 都持有最新值;但 BehaviorSubject getValue() 是同步的,StateFlow 读 value 属性 | | PublishSubject | SharedFlow(replay=0) | 都不重放;但 PublishSubject 没有 buffer,直接 onNext 会推给当前订阅者 | | ReplaySubject(n) | SharedFlow(replay=n) | 都重放 n 条;ReplaySubject 用 observeOn 异步转发,SharedFlow 同步发射 | | Completable / Single | Flow<Unit> / Deferred | 前者"不发射数据只表示完成",后者仍发射 |
这里有一个最易错的点:PublishSubject 与 SharedFlow(replay=0) 不等价。因为 SharedFlow 默认 extraBufferCapacity = 0 且发射是挂起的,慢订阅者会让上游挂起;而 PublishSubject 的 onNext 是非阻塞的、直接丢掉给不了的人。也就是说,SharedFlow(replay=0, extraBufferCapacity=0) 反而更接近 Flow 的冷流语义,而要模拟 PublishSubject 需要 MutableSharedFlow(replay=0, extraBufferCapacity = N, onBufferOverflow = DROP_LATEST)。
observeOn vs flowOn 也是高频对比点。observeOn 影响它下游的所有算子;flowOn 只影响它上游。Flow 官方推荐 flowOn,因为调度边界更明确、并发更好推理。
真实工程场景的推演
场景一:网络请求 + 重试。Rx 写法链长但控制精细:
// Rx
api.fetch()
.retryWhen {
errors -> errors.zipWith(Observable.range(1,3)) {
e, i -> Pair(e,i) }
.flatMap {
delay(1000L * it.second) } }
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
render(it) }, {
showError(it) })
// Flow
flow {
emit(api.fetch()) }
.retryWhen {
cause, attempt -> if (attempt < 3) {
emit(delay(1000L * attempt)); true } else false }
.flowOn(Dispatchers.IO)
.flowOn(Dispatchers.Main) // 或 collect 时切主线程
.onEach {
render(it) }
.catch {
showError(it) }
Flow 版本少了 subscribe 的两个回调(走 catch + onEach),异常处理更显式。
场景二:搜索防抖。Rx 用 debounce + switchMap(对应 Flow 的 flatMapLatest),语义一致:
// Rx
editText.textChanges().debounce(300).switchMap {
flow {
emit(search(it)) } }
// Flow
queryFlow.debounce(300).flatMapLatest {
key -> flow {
emit(search(key)) } }
场景三:Rx 仍占优的场景。①需要 observeOn 在链中间反复切换调度(Flow 里要多次 flowOn,可读性反而变差);②依赖 RxJava 特有算子(如 window、groupJoin、复杂 combineLatest 变体);③团队 Rx 资产重、迁移成本高于收益。此时混用(老模块 Rx、新模块 Flow,通过 rxFlow.asFlow() / flow.asObservable() 桥接)是常见过渡方案。
场景四:Subject 的迁移陷阱。代码里有个 PublishSubject<UserEvent>,多处 onNext 推送。直接换成 MutableSharedFlow(replay=0) 会出两个问题:一是发射处变成挂起调用,原代码里 subject.onNext(x) 是非挂起的,改成 emit 就得放进协程;二是没有 buffer 时的丢数据行为不同。修法:用 tryEmit + 显式 extraBufferCapacity,并在发射处包一层 launch。
最常见的坑是
把 RxJava 的 Subjects 语义直接套 SharedFlow,重放与错误处理策略不一致。 修法:迁移时逐个对照表核查——replay 数值、onBufferOverflow 策略、发射是否挂起、错误走 onError 还是转成数据。不查表直接换类型,是这一题最常见的迁移事故。
其次是用 flowOn 替代 observeOn 时位置放错。observeOn 影响下游,flowOn 影响上游。放错的表现是"线程没切过来",且不报错,很难查。
还有一个更隐蔽的坑:把 Flow 当成"语法更漂亮的 Rx",忽略了设计差异。Flow 的背压来自挂起,因此在 emit 之前做重活会阻塞下游;而 Rx 的调度是显式的。这会导致迁移后性能特征变化(原来靠 subscribeOn 扛住的重活,现在在 flowOn 上游还是挡着下游)。
现场手写这一段就够了
// 1) Subject → SharedFlow 的正确对照
// Rx: private val subject = PublishSubject<UserEvent>()
// Kotlin 对应:
private val _events = MutableSharedFlow<UserEvent>(
replay = 0,
extraBufferCapacity = 16, // PublishSubject 无缓冲,这里要给
onBufferOverflow = BufferOverflow.DROP_LATEST
)
val events: SharedFlow<UserEvent> = _events.asSharedFlow()
fun push(e: UserEvent) {
// Rx 里 subject.onNext(e) 是非挂起的;Flow 里 emit 挂起
if (!scope.isActive) return
scope.launch {
_events.emit(e) } // 发射处必须进协程
}
// 2) BehaviorSubject → StateFlow
// Rx: private val state = BehaviorSubject<User>(User.EMPTY)
private val _state = MutableStateFlow(User.EMPTY)
val state: StateFlow<User> = _state.asStateFlow()
fun setUser(u: User) {
_state.value = u } // 同步赋值,不需要协程
// 3) 背压策略的对应关系
// Rx: .onBackpressureBuffer() → Flow: .buffer(Channel.UNLIMITED) 或默认
// Rx: .onBackpressureDrop() → Flow: .buffer(capacity, DROP_LATEST)
// Rx: .onBackpressureLatest() → Flow: .conflate() 或 buffer(1, DROP_OLDEST)
// Rx: .observeOn(IO) → Flow: .flowOn(IO) ⚠️ 位置语义相反
// 4) 迁移期混用桥接
fun bridgeToRx(f: Flow<Result>): Observable<Result> = f.asObservable()
fun bridgeFromRx(o: Observable<Result>): Flow<Result> = o.asFlow()
// 5) 迁移检查清单(建议逐条走一遍)
fun migrate(old: PublishSubject<E>, scope: CoroutineScope) {
// ① replay 数值对齐
// ② BufferOverflow 策略对齐:DROP_LATEST ≈ Subject 无缓冲时的丢弃
// ③ 发射是否挂起:把 onNext 调用点包进 launch
// ④ 错误路径:Subject 用 onError 终止;Flow 只能用 catch 兜,转成数据
// ⑤ 订阅时机:Subject 订阅前的 onNext 直接丢;SharedFlow 同样但不阻塞上游
check(old.hasObservers())
scope.launch {
old.onStart {
_events.emit(it) }.collect {
_events.emit(it) } }
}
关键行解读:MutableSharedFlow 的 extraBufferCapacity 补上 PublishSubject 没有的缓冲;emit 挂起所以发射点要进协程;value 赋值是同步的对应 BehaviorSubject.getValue();buffer/conflate 对应三种背压策略;asObservable/asFlow 用于过渡期混用。
面试追问四连
"Flow 能完全替代 RxJava 吗?" 答:大部分场景可以(网络、状态、UI 事件);observeOn 精确到下游、多流复杂组合、Rx 特有算子这三块 Flow 表达力略弱。团队存量 Rx 资产重时,混用过渡比强推迁移更稳。
"Flow 的背压和 onBackpressureBuffer 什么关系?" 答:Flow 靠 emit 挂起实现天然的拉模式背压,不需要策略枚举;buffer/conflate 是显式指定缓冲大小与丢弃策略的补充手段。Rx 的策略是显式协议,Flow 是语言层面的机制。
"PublishSubject 等于 SharedFlow(replay=0) 吗?" 答:不等于。PublishSubject.onNext 非挂起、无缓冲;SharedFlow(replay=0) 默认 extraBufferCapacity=0 且 emit 挂起。语义更接近的是 MutableSharedFlow(replay=0, extraBufferCapacity=N, onBufferOverflow=DROP_LATEST)。
"flowOn 和 observeOn 有什么区别?" 答:observeOn 影响它下游的算子;flowOn 只影响它上游。Flow 推荐 flowOn,因为调度边界明确;多个 flowOn 可把流水线切成多段。
落地建议
1. 迁移前先建对照表(Subject 类型 → replay/策略/挂起语义/错误路径),逐条核对,不要凭印象换类型。 2. 新代码统一 Flow,老 Rx 模块用 asFlow/asObservable 桥接,不做无必要的全量改写。 3. 代码评审新增一条:出现 Subject/onNext 时必须在注释里说明对应的 Flow 类型与策略,便于以后替换。 4. 自动化预防:为迁移后的流写 Turbine 单测,断言重放行为(订阅前发射能否收到)与错误恢复次数;并发场景补"发射方不被慢订阅者阻塞"的用例。
给正在准备面试的你 把这题画成"背压机制对比图"。左侧 Rx:三个算子方块串在一起,下方挂一个"背压策略下拉框",列出 onBackpressureBuffer/Drop/Latest 三个选项,旁边注"需要显式选协议"。右侧 Flow:三个算子方块,箭头中段标一个"挂起"标记(画一个小暂停图标),旁边注"emit 挂起 → 天然反压,无需选协议"。图下方再画一张 Subject 对应表:四行分别连 BehaviorSubject→StateFlow、PublishSubject→SharedFlow(策略化)、ReplaySubject(n)→SharedFlow(replay=n)、Completable→Flow,PublishSubject 那行标红"⚠️ 需调策略才等价"。
再补一个高分延伸:"为什么 Kotlin 选择用挂起而不是显式背压协议?" 因为挂起函数把"等待"变成了语言层面的控制流,编译器能保证"不会阻塞线程",同时背压自动生效,不需要每个操作符都实现一遍策略。这减少了算子实现的复杂度(Rx 里每个算子都要考虑 request(n)),代价是失去了"用策略表达不同业务意图"的灵活性——比如"满了就丢最新的 UI 状态"和"满了就慢下来"这两种需求,在 Flow 里要分别用 conflate 和默认挂起来表达。答出"语言机制 vs 协议设计"这层权衡,基本就是资深水平。
复习时别孤立刷题:Mutex 与信号量——上一节讲并发资源的保护,本节把话题提升到"库与库之间的选型",同样是在回答"什么时候该用哪一个"。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:Mutex-与信号量:协程世界的锁
下一篇预告:Kotlin-Multiplatform-初识:跨端共享业务逻辑
有任何问题欢迎在评论区留言交流。