【RxSwift】Swift 版 ReactiveX,响应式编程优雅处理异步事件流
iOS三方库精读 · 第 8 期
一、一句话介绍
RxSwift 是 ReactiveX 的 Swift 实现,将异步操作和事件统一为可观察序列(Observable),通过操作符进行声明式组合变换,极大简化复杂异步逻辑。
| 属性 | 值 |
|---|---|
| GitHub Stars | 24.5k+ |
| 最新版本 | 6.7.0 |
| License | MIT |
| 支持平台 | iOS 9+ / macOS 10.10+ / watchOS / tvOS |
二、为什么选择它
原生痛点
在没有 RxSwift 之前,处理异步事件流往往面临:
- 回调地狱:嵌套的网络请求、多层 callback 难以维护
- 状态同步:UI 与数据模型的双向绑定需要大量 KVO / Notification / Delegate 样板代码
- 线程切换:GCD 和 OperationQueue 手动管理容易出错
- 错误处理:分散在各处的 try-catch,遗漏处理导致崩溃
- 事件取消:Timer、Notification 观察者忘记移除,造成内存泄漏
RxSwift 核心优势
- 统一异步模型:网络请求、通知、KVO、Timer、手势统统归为 Observable,一套 API 走天下
- 声明式组合:通过 map/filter/flatMap 等操作符链式调用,代码如流水般清晰
- 自动资源管理:DisposeBag 机制确保订阅随生命周期自动释放
- 丰富的操作符:100+ 操作符覆盖变换、过滤、组合、错误处理、调度等场景
- RxCocoa 扩展:UIKit 控件开箱即用的双向绑定(UITextField、UIButton、UITableView 等)
原生 API vs RxSwift
| 场景 | 原生方式 | RxSwift 方式 |
|---|---|---|
| 网络请求 | URLSession + closure 嵌套 | Observable + flatMap 链式 |
| 输入框实时搜索 | addTarget + Timer 去抖 | rx.text + debounce |
| 表单验证 | 多个 KVO / Delegate | combineLatest 一行搞定 |
| Timer 管理 | Timer.scheduledTimer + invalidate | Observable.interval + disposed(by:) |
| 通知监听 | NotificationCenter + removeObserver | NotificationCenter.rx.notification |
三、核心功能速览
基础层 概念解释、环境配置、基础用法
环境配置
Swift Package Manager(推荐)
swift
// Package.swift
dependencies: [
.package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.7.0")
]
CocoaPods
ruby
pod 'RxSwift', '~> 6.7'
pod 'RxCocoa', '~> 6.7' # UIKit 扩展
pod 'RxRelay', '~> 6.7' # Relay 组件
核心概念:Observable 三部曲
swift
import RxSwift
let disposeBag = DisposeBag()
// 1. 创建 Observable
let observable = Observable<String>.create { observer in
observer.onNext("Hello")
observer.onNext("RxSwift")
observer.onCompleted()
return Disposables.create()
}
// 2. 订阅
observable
.subscribe(
onNext: { print("收到: \($0)") },
onCompleted: { print("完成") }
)
// 3. 管理 disposal
.disposed(by: disposeBag)
// 输出:
// 收到: Hello
// 收到: RxSwift
// 完成
进阶层 最佳实践、性能优化、线程安全
Subject 与 Relay
swift
// PublishSubject: 只收到订阅后的事件
let publish = PublishSubject<String>()
publish.onNext("A") // 不会收到
publish.subscribe { print($0) } // 订阅
publish.onNext("B") // ✅ 收到
// BehaviorSubject: 保留最新一个值,新订阅者立即收到
let behavior = BehaviorSubject(value: "初始值")
behavior.onNext("新值")
behavior.subscribe { print($0) } // ✅ 立即收到 "新值"
// BehaviorRelay: UI 安全,永不触发 error/completed
let relay = BehaviorRelay(value: "初始值")
relay.accept("更新值") // 用 accept 替代 onNext
relay.asObservable().subscribe { print($0) }
常用操作符
swift
// 过滤
Observable.of(1, 2, 3, 4, 5)
.filter { $0 % 2 == 0 } // [2, 4]
// 变换
Observable.of(1, 2, 3)
.map { $0 * 2 } // [2, 4, 6]
// 去重
Observable.of("a", "b", "a", "c")
.distinctUntilChanged() // ["a", "b", "a", "c"] - 相邻去重
// 扁平化(网络请求链式)
searchText
.flatMapLatest { query in
return networkAPI.search(query) // 自动取消上一个请求
}
// 组合
Observable.combineLatest(
usernameValid,
passwordValid
) { $0 && $1 } // 表单验证
线程调度
swift
Observable<String>.create { observer in
// 后台执行耗时任务
Thread.sleep(forTimeInterval: 1)
observer.onNext("计算结果")
observer.onCompleted()
return Disposables.create()
}
.subscribe(on: ConcurrentDispatchQueueScheduler(qos: .background)) // 订阅线程
.observe(on: MainScheduler.instance) // 观察线程
.subscribe(onNext: { value in
// UI 更新在主线程
label.text = value
})
.disposed(by: disposeBag)
深入层 源码解析、设计思想、扩展定制
核心协议关系
scss
ObservableType (协议)
↓
Observable (class)
↓ 创建
AnyObserver (观察者抽象)
↓ 订阅
Disposable (取消订阅)
↓ 持有
DisposeBag (资源回收容器)
Observer 模式实现
swift
// Observable.subscribe 核心流程
func subscribe(_ observer: Observer) -> Disposable {
// 1. 包装 observer
let disposable = Disposables.create()
// 2. 调用核心生产逻辑
let sink = AnonymousSink(observer: observer, dispose: disposable)
// 3. 返回 disposable 用于取消
return disposable
}
// dispose 时移除订阅
final class DisposeBag {
var disposables: [Disposable] = []
deinit {
disposables.forEach { $0.dispose() }
}
}
四、实战演示
场景:实时搜索 + 表单验证 + TableView 绑定
swift
import UIKit
import RxSwift
import RxCocoa
final class LoginViewController: UIViewController {
private let disposeBag = DisposeBag()
// UI
private let emailField = UITextField()
private let passwordField = UITextField()
private let loginButton = UIButton(type: .system)
private let tableView = UITableView()
// 数据源
private let suggestions = BehaviorRelay<[String]>(value: [])
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
}
private func setupBindings() {
// 1. 实时搜索(防抖 500ms + 去重)
emailField.rx.text.orEmpty
.debounce(.milliseconds(500), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.filter { !$0.isEmpty }
.flatMapLatest { [weak self] query -> Observable<[String]> in
// 模拟搜索建议 API
return self?.searchSuggestions(query) ?? .just([])
}
.bind(to: suggestions)
.disposed(by: disposeBag)
// 2. 表单验证(多输入组合)
let emailValid = emailField.rx.text.orEmpty
.map { $0.contains("@") && $0.contains(".") }
let passwordValid = passwordField.rx.text.orEmpty
.map { $0.count >= 6 }
Observable.combineLatest(emailValid, passwordValid) { $0 && $1 }
.bind(to: loginButton.rx.isEnabled)
.disposed(by: disposeBag)
// 3. TableView 数据绑定
suggestions
.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { _, text, cell in
cell.textLabel?.text = text
}
.disposed(by: disposeBag)
// 4. 点击事件
loginButton.rx.tap
.withLatestFrom(Observable.combineLatest(
emailField.rx.text.orEmpty,
passwordField.rx.text.orEmpty
))
.subscribe(onNext: { [weak self] email, password in
self?.performLogin(email: email, password: password)
})
.disposed(by: disposeBag)
}
private func searchSuggestions(_ query: String) -> Observable<[String]> {
// 模拟 API 请求
return Observable.just(["\(query)@gmail.com", "\(query)@icloud.com"])
.delay(.milliseconds(300), scheduler: MainScheduler.instance)
}
private func performLogin(email: String, password: String) {
print("登录: \(email), 密码: \(password)")
}
}
五、源码亮点
进阶层 值得借鉴的用法
操作符链式调用的 Builder 模式
swift
// 每个操作符返回新的 Observable,支持无限链式
observable
.map { transform($0) } // 返回 Map<Source>
.filter { predicate($0) } // 返回 Filter<Map<Source>>
.subscribe { ... } // 返回 Disposable
takeUntil 实现自动取消
swift
// 当 self.deallocated 时自动取消网络请求
networkRequest()
.takeUntil(self.rx.deallocated)
.subscribe(onNext: { ... })
深入层 设计思想解析
Producer-Consumer 模式
swift
// Observable 是 Producer,产生事件
class Producer<Element>: Observable<Element> {
func run(_ observer: Observer, cancel: Cancel) -> Sink {
// 子类实现具体生产逻辑
}
}
// Sink 是 Consumer,消费事件并管理生命周期
class Sink<Observer: ObserverType>: Disposable {
let observer: Observer
var disposed = false
func dispose() {
disposed = true
}
}
操作符的实现模式
以 map 为例:
swift
final class MapSink<Source, Result>: Sink<Result>, ObserverType {
typealias Element = Source
private let transform: (Source) -> Result
func on(_ event: Event<Source>) {
switch event {
case .next(let element):
let result = transform(element) // 变换
forwardOn(.next(result)) // 传递给下游
case .error(let error):
forwardOn(.error(error))
case .completed:
forwardOn(.completed)
}
}
}
六、踩坑记录
问题 1:订阅未释放导致内存泄漏
问题:在 ViewController 中订阅 Observable,页面释放后订阅仍在执行。
原因:未将 Disposable 加入 DisposeBag,或 DisposeBag 生命周期与 VC 不一致。
解决:
swift
// ❌ 错误:未持有 Disposable
observable.subscribe { ... }
// ✅ 正确:加入 DisposeBag
private let disposeBag = DisposeBag()
observable.subscribe { ... }
.disposed(by: disposeBag)
问题 2:UI 更新不在主线程
问题:后台网络请求回调中更新 UI 导致崩溃。
原因:默认情况下 Observable 继承订阅者的线程上下文。
解决:
swift
// ❌ 错误:可能在后台线程
networkRequest()
.subscribe(onNext: { label.text = $0 })
// ✅ 正确:显式切换到主线程
networkRequest()
.observe(on: MainScheduler.instance)
.subscribe(onNext: { label.text = $0 })
问题 3:flatMap 与 flatMapLatest 混淆
问题:快速输入搜索关键词,收到旧的请求结果。
原因 :flatMap 会保留所有内部 Observable,flatMapLatest 会自动取消上一个。
解决:
swift
// ❌ 错误:旧请求可能覆盖新结果
searchText.flatMap { searchAPI($0) }
// ✅ 正确:自动取消上一个请求
searchText.flatMapLatest { searchAPI($0) }
问题 4:Subject 发送 completed 后无法复用
问题 :PublishSubject 发送 .completed 后,后续订阅收不到事件。
原因:Subject 一旦 terminated,状态不可逆转。
解决:
swift
// 方案 1:使用 Relay(不发送 completed)
let relay = PublishRelay<String>()
// 方案 2:重新创建 Subject
func resetSubject() {
subject = PublishSubject<String>()
}
问题 5:share(replay:) 重复执行副作用
问题:多个订阅者导致网络请求被执行多次。
原因:默认每个订阅者独立触发 Observable 执行。
解决:
swift
// ❌ 错误:每个订阅触发一次请求
let request = api.fetchData()
request.subscribe { ... } // 请求 1
request.subscribe { ... } // 请求 2
// ✅ 正确:共享执行结果
let request = api.fetchData()
.share(replay: 1) // 缓存最近 1 个结果
request.subscribe { ... } // 请求 1
request.subscribe { ... } // 复用结果
问题 6:withUnretained 造成循环引用
问题 :使用 withUnretained(self) 仍出现循环引用。
原因:闭包内额外强引用了 self。
解决:
swift
// ❌ 错误:闭包内强引用
observable
.withUnretained(self)
.subscribe { self, value in
self.items.append(value) // 强引用
}
// ✅ 正确:使用 weak 或确保无循环
observable
.subscribe(onNext: { [weak self] value in
self?.items.append(value)
})
七、延伸思考
RxSwift vs Combine 横向对比
| 维度 | RxSwift | Combine |
|---|---|---|
| 开发商 | 社区 (ReactiveX) | Apple 官方 |
| 最低版本 | iOS 9+ | iOS 13+ |
| 操作符数量 | 100+ 极其丰富 | ~50 够用但较少 |
| UI 绑定 | RxCocoa 内建 | 需自行封装或用 SwiftUI |
| 调试支持 | RxSwift.Resources / debug() | print() / handleEvents() |
| 学习曲线 | 较陡(概念多) | 中等 |
| 包体积 | ~2MB | 系统内建,0 额外 |
| SwiftUI 集成 | 需桥接 | 原生支持 |
| 维护状态 | 活跃 | Apple 官方维护 |
选型建议
选 RxSwift:
- 项目需要兼容 iOS 13 以下
- 团队有 RxJava / RxJS 经验,希望统一范式
- 需要大量 UIKit 双向绑定(RxCocoa 非常成熟)
- 需要
withLatestFrom等 Combine 缺失的操作符
选 Combine:
- 纯 SwiftUI 项目,Combine 原生集成最流畅
- 不想引入第三方依赖,减少包体积
- 新项目最低版本 ≥ iOS 13
迁移建议
对于已有 RxSwift 项目:
- 短期内无需迁移,RxSwift 维护状态良好
- 新增 SwiftUI 页面可用 Combine,与 RxSwift 共存
- 使用
RxCombine库实现互相转换
八、参考资源
官方资源
推荐文章
- RxSwift 中文文档 - 独酌 | 2020
- RxSwift 核心概念 - Dylan Reyes | 2022
系列 Demo 仓库
- github.com/ios-lib-dem... - 本系列配套代码
本期互动
小作业
尝试用 RxSwift 实现一个「搜索建议」功能:输入框输入时防抖 300ms,发起网络请求获取建议列表,展示在 UITableView 中,并在评论区贴出你的关键代码片段。
思考题
如果让你从零实现 RxSwift 的 debounce 操作符,你会如何设计?需要考虑哪些边界情况?
读者征集
你在使用 RxSwift 时踩过哪些坑?欢迎评论区分享,优质回答会收录进下一期《踩坑记录》。
📅 本系列每周五晚更新 · 已学习:[✓ Alamofire] [✓ Kingfisher] [✓ Lottie] [✓ MarkdownUI] [✓ SDWebImage] [✓ SnapKit] [✓ ListDiff] [→ RxSwift] [○ Charts]