SDK 与源码理解
Rxswift&combine

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;避免循环引用。
  • 测试:隔离副作用、使用测试调度器、固定时间与随机性。