RxSwift & Combine
一、核心概念速记
响应式编程的本质是什么?与传统回调/代理相比优势在哪里?
- **答案:**以“数据流 + 声明式依赖”为中心,事件作为流在时间维度上传播;订阅者只描述“当数据变化时该做什么”。优势:1)消除回调地狱与状态分散;2)组合/变换能力强(map/flatMap…);3)可预期的线程切换(Schedulers);4)统一错误与完成语义;5)更易测试与解耦(输入输出可模拟)。
RxSwift 与 Combine 的关系与差异?
答案:
- **相同点:**可观察序列/发布者模型、算子集合、调度器、背压/节流思路、单元测试思维一致。
- **不同点:**Combine 为苹果原生(iOS 13+),强类型 Publisher/Subscriber + Failure 关联类型;RxSwift 跨平台更早成熟、Trait(Single/Maybe/Completable/Driver/Signal)完善、社区生态大。Combine 有 AnyPublisher 擦型、@Published、assign/sink;背压用 Demand,而 RxSwift 常用节流/缓冲规避。
二、核心类型与语义
RxSwift:Observable/Single/Completable/Maybe/Driver/Signal 分别何用?
答案:
- Observable
:0..n next + 可 error/complete(通用)。 - Single
:要么发出一个元素要么 error(典型:一次请求)。 - Completable:无元素,只有完成或错误(典型:写库、保存)。
- Maybe
:0 或 1 个元素或 error(可选值语义)。 - Driver
:主线程、无 error、share(replay:1, scope:.whileConnected)(UI 绑定)。 - Signal
:主线程、无 error、不重放(适合一次性事件,如点击)。
Combine:Publisher/AnyPublisher/@Published/Subject 有何区别?
答案:
- Publisher:协议,定义输出 Output 和失败类型 Failure。
- AnyPublisher<Output, Failure>:类型擦除,隐藏上游细节,利于 API 暴露。
- @Published:属性包装器,自动生成 Publisher,适合 ViewModel 状态。
- PassthroughSubject:只转发新值,不缓存。
- CurrentValueSubject:保存最新值并对新订阅者重放一次。
三、线程与调度(Schedulers)
RxSwift 的线程切换如何写?哪些坑要避免?
答案:
- 规则:上游工作用 subscribe(on:),下游观察用 observe(on:)。
api.fetch()
.subscribe(on: ConcurrentDispatchQueueScheduler(qos: .userInitiated))
.map(parse)
.observe(on: MainScheduler.instance)
.bind(to: uiBinder)
.disposed(by: bag)- 避坑:1)别只用 observe(on:) 以为切了上游线程;2)UI 更新必须回主线程;3)避免在主线程执行重活。
Combine 如何切线程?
答案:
- subscribe(on:) 指定上游执行队列;receive(on:) 指定下游回调队列。
publisher
.subscribe(on: backgroundQueue)
.map(parse)
.receive(on: DispatchQueue.main)
.sink { _ in } receiveValue: { ... }
.store(in: &cancellables)四、运算符高频考点
map / flatMap / switchLatest(Rx)或 switchToLatest(Combine)的区别?
答案:
- map:一对一,同步变换。
- flatMap:一对多,映射为新流并合并所有子流(可能并发)。
- flatMapLatest(Rx)/ switchToLatest(Combine):只订阅最新子流,自动取消旧流(典型:搜索联想)。
merge / combineLatest / zip 的区别?
答案:
- merge:同类型流交错输出,谁来先发谁。
- combineLatest:任一源更新,输出最新组合(需每源至少发过一次)。
- zip:按配对输出,第 n 个和第 n 个成对;节拍器。
throttle vs debounce 的区别?
答案:
- debounce(t):静默 t 后才发(输入停止后触发,防抖)。
- throttle(t):在窗口 t 内最多发一次(限流)。
withLatestFrom(Rx)/ combineLatest + sample(Combine 替代)用途?
- *答案:**当触发源 A 来时,取 B 的最新值输出(典型:点击提交时读取当前表单状态)。
五、热序列与冷序列
什么是“热(Hot)”与“冷(Cold)”?
答案:
- Cold:每个订阅者各自独立的事件序列(如 Observable.create、URLSession 请求)。
- Hot:事件独立于订阅而持续发生(如 Subject、通知、定时器)。
- UI 绑定宜用 热 且共享副作用(share/multicast/replay)。
六、错误处理与重试
RxSwift/Combine 常见错误处理手段?
答案:
- 捕获与恢复:catchErrorJustReturn(Rx)、catch(Combine)。
- 重试:retry(_:);指数退避可用 retryWhen(Rx)或自定义递增延迟(Combine)。
- 终止 vs 恢复:UI 流建议“错误 -> 状态值”而非终止(Driver/Signal、Failure == Never)。
// Combine 恢复为占位数据
api.publisher()
.retry(2)
.catch { _ in Just(placeholder).setFailureType(to: Error.self) }
.eraseToAnyPublisher()七、背压与流量控制
Combine 的 Demand 是什么?RxSwift 如何应对背压?
答案:
- Combine:Subscriber 通过 demand 拉取元素(背压),可增量请求,减少上游压力。
- RxSwift:无显式背压协议,常通过 buffer/window/sample/throttle/debounce、批量处理、丢弃策略(buffer(count:skip:))控制节奏;I/O 层面用队列/批请求。
八、订阅生命周期与内存管理
RxSwift 为何常见循环引用?如何避免?
答案:
- 原因:闭包捕获 self 与订阅持有者相互引用。
- 解决:[weak self] / unowned self、在 ViewModel 暴露驱动型 trait(Driver)、使用 DisposeBag 绑定到对象生命周期、或 takeUntil(deallocated)。
button.rx.tap
.withLatestFrom(viewModel.state)
.subscribe(onNext: { [weak self] in self?.render($0) })
.disposed(by: bag)Combine 的取消与资源释放如何做?
**答案:**AnyCancellable 存入集合,在对象 deinit 时释放;或使用 assign(to: &$state) 与 SwiftUI 生命周期配合自动释放。
九、Subjects / Relays / Publishers
RxSwift:Subject 与 Relay 的区别?
答案:
- PublishSubject:只发订阅后事件,不重放。
- BehaviorSubject:持有最新值并重放。
- ReplaySubject:重放指定个数历史。
- Relay(PublishRelay/BehaviorRelay):不发送 error/complete,适合 UI;accept(_:) 推值。
Combine 中用什么替代 Relay?
**答案:**CurrentValueSubject(≈BehaviorRelay/Subject)与 PassthroughSubject(≈PublishRelay/Subject);不发送完成/错误的场景可用 eraseToAnyPublisher + 约定或包装为 Failure == Never。
十、UI 绑定与架构
UIKit 中做双向绑定的稳妥写法?
答案:
- 输入:textField.rx.text.orEmpty.changed / Combine 的 textPublisher(第三方)或 UIControl 扩展。
- 输出:Driver/receive(on: .main) 更新 UI。
- 双向:防止回环,常用 distinctUntilChanged + 只在用户输入路径写回 VM。
// RxSwift:VM <- UI
textField.rx.text.orEmpty
.distinctUntilChanged()
.bind(to: viewModel.username)
.disposed(by: bag)
// UI <- VM(Driver 保证主线程、无错误)
viewModel.usernameOutput
.drive(textField.rx.text)
.disposed(by: bag)SwiftUI + Combine 常见模式?
答案:@State(视图局部)、@StateObject/@ObservedObject(VM)、@Published(可观察状态)、onReceive 订阅外部流,Task 结合 async/await 与 Combine 互操作。
十一、多播与共享副作用
何时用 share/multicast/replay?
答案:
- **问题:**上游副作用(网络/计算)在多个订阅时被重复触发。
- 解法:
- Rx:share()(默认 whileConnected)、share(replay:1)、multicast(Subject) 控制连接点。
- Combine:share()、multicast(subject:)、makeConnectable() + connect()。
- 经验:网络请求结果被多个订阅使用时要“热化 + 重放 1”。
十二、场景题(可直接背)
场景1:搜索框防抖 + 取消旧请求
答案:
- RxSwift:debounce + distinctUntilChanged + flatMapLatest。
- Combine:debounce + removeDuplicates + map -> switchToLatest。
// Combine
let results = query
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.map { api.search($0) } // -> AnyPublisher<[Item], Error>
.switchToLatest()
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()场景2:按钮点击触发提交,但要拿到“最新表单值”
答案:
- RxSwift:button.tap.withLatestFrom(formState)。
- Combine:buttonTap.combineLatest(formState).map { _, s in s }.sample(on: buttonTap) 或自定义 withLatestFrom 扩展。
场景3:分页加载避免并发请求堆叠
答案:
- 用 flatMapLatest(Rx)/ switchToLatest(Combine)取消旧请求;或用状态机串行化(serial queue + flatMap(maxPublishers: .max(1)))。
十三、性能与最佳实践
常见性能问题与优化手段?
答案:
- 过度主线程工作 → subscribe(on:)/后台解析。
- 过多订阅与重复副作用 → share/replay,减少重复订阅。
- 高频事件(滚动/打点)→ throttle/sample 降频;必要时合批。
- 大图/JSON 解析 → 背景队列 + 流式解析(分块 map)。
- 可测试性:将副作用边界(网络、磁盘、时间)抽象为协议,测试注入 TestScheduler/ImmediateScheduler.
十四、RxSwift 与 Combine 互操作
在 iOS 13+ 同时兼容两套栈的策略?
答案:
- 对外暴露协议/闭包,不直接暴露具体 Publisher/Observable。
- 写一层适配:Rx 层 asPublisher()(第三方)或 Combine 层 eraseToAnyPublisher();模块边界中只走纯 Swift 类型(数组、模型、Result)。
- 逐步迁移:VM 先用 Combine,UIKit 控制器可借 UIControl Combine 扩展;旧模块继续 Rx,边界处做桥接。
十五、易错点快答
为什么 Driver / Signal 不会发出 error?
- **答案:**UI 流不应因错误终止;错误应转化为“状态值”(如 toast、占位),保持流存活与主线程保证。
subscribe(on:) 和 observe(receive:on:) 顺序重要吗?
- **答案:**重要。subscribe/subscribe(on:) 影响上游起点,放得越靠上影响越大;observe/onReceive 影响之后的下游回调。
Combine assign(to:on:) 与 assign(to: &$state) 的区别?
- **答案:**前者 KVC 风格、返回 AnyCancellable;后者 SwiftUI 友好,基于 @Published 的投递并自动管理生命周期。
retry 与 catch 的先后顺序?
- **答案:**通常 retry 放在 catch 之前;先尝试重试,最终用 catch 提供降级数据或错误路径。
十六、简短代码题(面试白板版)
写一个节流点击(2 秒内只响应一次)
// RxSwift
button.rx.tap
.throttle(.seconds(2), scheduler: MainScheduler.instance)
.bind(onNext: submit)
.disposed(by: bag)将请求结果缓存并多处复用(只请求一次)
// RxSwift
let shared = api.loadConfig()
.share(replay: 1, scope: .whileConnected)
shared.bind(to: vmA.config).disposed(by: bag)
shared.bind(to: vmB.config).disposed(by: bag)Combine:搜索联想 + 主线程更新
let suggestions = query
.debounce(for: .milliseconds(250), scheduler: RunLoop.main)
.removeDuplicates()
.map(api.suggest) // -> AnyPublisher<[String], Never>
.switchToLatest()
.receive(on: DispatchQueue.main)
.assign(to: &$viewModel.suggestions)十七、面试“元问题”回答模板
你们为什么选择 RxSwift/Combine?如何落地到架构?
答案要点:
1)一致的 async API 面:输入输出声明式,降低状态爆炸;
2)线程语义清晰:上游/下游各司其职;
3)可测试:替换上游、用 TestScheduler;
4)可维护:复杂需求靠算子组合而非 if-else;
5)迁移策略:新模块 Combine,旧模块 Rx 保持稳定,边界适配;
6)工程规范:UI 层仅接收 Driver/Failure == Never,ViewModel 暴露只读流,网络层与存储层用纯 Swift 类型与解码模型。
十八、Checklist(落地规范,背就完了)
- UI 绑定:主线程、无 error(Rx 用 Driver/Signal;Combine 用 Failure == Never + receive(on: .main))。
- 线程:上游 subscribe(on:),下游 observe/receive(on:)。
- 共享:副作用请求 share(replay:1)/multicast。
- 背压:debounce/throttle/sample/buffer,必要时批处理。
- 错误:retry→catch→状态降级;UI 不因错误终止。
- 内存:[weak self] + DisposeBag / AnyCancellable;避免循环引用。
- 测试:隔离副作用、使用测试调度器、固定时间与随机性。