普通视图

发现新文章,点击刷新页面。
昨天以前首页

MVVM 本质解构 + RxSwift 与 Combine 深度对决与选型指南

作者 择势
2026年4月19日 13:47

作为 iOS 开发演进的核心架构,MVVM彻底解决了原生 MVC 的 Massive View Controller 顽疾;而响应式编程是 MVVM 落地的灵魂 —— 脱离响应式的 MVVM 只是伪架构。本文从资深开发工程化视角,深度拆解 MVVM 的底层设计逻辑,全方位对比 RxSwift 与 Combine 两大 iOS 响应式框架,结合实战、踩坑与选型策略,为中大型 iOS 项目的架构设计提供专业参考。

一、深刻理解 MVVM:不止是分层,是 iOS UI 开发的范式升级

绝大多数 iOS 开发者对 MVVM 的理解停留在「View-ViewModel-Model」三层结构,这是表层认知。从资深开发和工程化角度,MVVM 的核心是UI 与业务逻辑的彻底解耦数据驱动 UI的编程范式升级。

1.1 原生 MVC 的致命困境

iOS 官方推荐的 MVC 架构,在实际工程中会快速腐化:

  • ViewController 身兼数职:UI 渲染、用户交互、网络请求、数据解析、业务逻辑、状态管理;
  • 千行 VC 是常态,不可测试、难复用、难维护
  • View 与 Model 强耦合,UI 修改会牵连业务逻辑,业务逻辑变动会破坏 UI 渲染。

这是 iOS 原生开发的历史痛点,也是 MVVM 诞生的核心原因。

1.2 MVVM 的核心本质(资深开发必掌握)

MVVM 的设计目标不是「分层」,而是让 UI 层彻底被动化,让业务逻辑彻底纯净化

核心角色职责(严格边界)

表格

角色 核心职责 禁忌
View(ViewController/UIView) 仅负责:转发用户交互事件、响应数据渲染 UI 不写任何业务逻辑、不直接操作 Model、不持有网络 / 数据库对象
ViewModel 核心中间层:持有 Model、处理业务逻辑(校验 / 网络 / 数据转换)、暴露可观察数据流 不导入 UIKit、不持有任何 UI 对象、完全脱离 iOS 平台,可独立单元测试
Model 纯数据结构(实体类 / 结构体) 不包含任何业务逻辑、不与 UI/ViewModel 耦合

MVVM 的灵魂:双向绑定

View 与 ViewModel 之间不直接调用方法,而是通过可观察数据流实现自动绑定:

  1. ViewModel 数据变化 → 自动驱动 View 更新 UI;
  2. View 用户交互(点击 / 输入)→ 自动触发 ViewModel 业务逻辑。

这是 MVVM 的核心价值,也是原生 iOS 无法高效实现的能力 ——KVO/Notification/Delegate 代码冗余、易泄漏、难以维护,必须依赖响应式编程框架落地。

1.3 MVVM 黄金法则(工程化落地准则)

  1. View 只做「UI 转发 + 渲染」,无任何业务逻辑;
  2. ViewModel 无 UIKit 依赖,100% 可单元测试;
  3. 所有通信通过响应式数据流,禁止反向引用;
  4. 单一职责:复杂 ViewModel 拆分 UseCase/Service,拒绝臃肿。

二、响应式编程:MVVM 的唯一高效落地方案

MVVM 的核心是「绑定」,而响应式编程(RP) 是实现绑定的最优解:

  • 一切异步事件(UI 点击、网络请求、数据变化、定时器)抽象为可观察的数据流
  • 声明式语法处理数据流,实现自动化绑定;
  • 彻底告别代理、通知、闭包嵌套的异步噩梦。

iOS 生态中,只有两个选择:

  1. RxSwift:跨平台响应式标准 ReactiveX 的 iOS 实现,成熟稳定;
  2. Combine:苹果原生官方响应式框架,iOS13 + 内置,未来主流。

三、RxSwift 深度解析:成熟的响应式事实标准

3.1 核心定位

RxSwift 是ReactiveX的 iOS 移植版本(跨平台响应式规范,Java/RxJS 通用),是 iOS 响应式编程的「事实标准」,历经多年迭代,生态极致完善。

3.2 核心抽象

  • Observable:数据流生产者(发送数据 / 错误 / 完成);
  • Observer:数据流消费者;
  • Disposable:资源回收器(避免内存泄漏);
  • Operator:操作符(map/filter/flatMap/zip),数据流处理核心;
  • Scheduler:线程调度器(主线程 / 后台线程切换)。

3.3 iOS 生态矩阵

  • RxCocoa:UIKit 全扩展(UIButton.rx.tap/UITextField.rx.text);
  • RxDataSources:UITableView/CollectionView 极简数据绑定;
  • RxAlamofire:网络请求响应式封装;
  • 几乎所有主流第三方库都提供 Rx 扩展。

3.4 优劣势

优势

  • 全版本兼容:iOS8+,覆盖所有存量项目;
  • 生态天花板:社区成熟,无实现不了的场景;
  • 操作符丰富:复杂数据流开箱即用;
  • 文档 / 社区完善,问题秒解。

劣势

  • 学习成本极高:冷 / 热 Observable、背压等概念抽象;
  • 第三方依赖:增加包体积;
  • 非官方维护,未来迭代放缓。

四、Combine 深度解析:苹果原生的响应式未来

4.1 核心定位

苹果在 iOS13 推出的原生响应式框架,深度集成 SwiftUI、UIKit、Swift Concurrency(async/await),是苹果生态的未来标准

4.2 核心抽象(与 RxSwift 无缝映射)

表格

RxSwift Combine 功能一致
Observable Publisher 数据流生产者
Observer Subscriber 数据流消费者
Disposable Cancellable 资源销毁
BehaviorSubject CurrentValueSubject 带缓存值
PublishSubject PassthroughSubject 无缓存值

4.3 原生杀手锏

  • @Published:属性包装器,一行代码生成可观察数据流,ViewModel 绑定极简;
  • 原生集成 GCD/Operation,线程调度零成本;
  • 无缝衔接 Swift Concurrency,现代 Swift 编程体验拉满。

4.4 优劣势

优势

  • 官方原生:无第三方依赖,系统级优化;
  • 轻量无体积:内置系统,无需引入库;
  • 语法极简:贴合 Swift 语法,学习成本低;
  • 未来兼容:随 Swift/SwiftUI 迭代,长期维护。

劣势

  • 版本硬限制:iOS13 以下完全不支持
  • 生态贫瘠:第三方库远少于 RxSwift;
  • 操作符精简:复杂场景需自定义。

五、RxSwift vs Combine:全方位深度对比(资深开发核心参考)

5.1 基础能力对比

表格

维度 RxSwift Combine
兼容性 iOS8+,全平台覆盖 iOS13+,低版本无支持
依赖方式 第三方库(CocoaPods/SPM) 系统内置,无依赖
语法风格 标准 ReactiveX 链式调用 Swift 原生语法,极简简洁
核心简化 无属性包装器,需手动创建 Subject @Published 一行实现绑定
生态完善度 极致完善(UI / 网络 / 列表全覆盖) 原生生态完善,第三方薄弱
背压支持 需额外处理 原生内置支持
错误处理 灵活,无强类型约束 强类型泛型约束,更安全
测试工具 RxTest/RxBlocking,功能强大 原生 XCTest,简洁轻量化
学习成本 高(ReactiveX 抽象概念) 低(Swift 原生,易上手)

5.2 性能与内存

  • Combine:系统级优化,内存占用更低,线程调度更高效;
  • RxSwift:社区优化多年,性能稳定,资源回收严格可控;
  • 内存管理:两者均需手动管理订阅(DisposeBag/Set),否则泄漏。

5.3 工程化适配

  • 存量旧项目 → RxSwift(兼容低版本);
  • 全新 SwiftUI 项目 → Combine(原生最佳搭配);
  • 团队新手 → Combine(学习成本低);
  • 复杂数据流 / 列表 → RxSwift(生态完善)。

六、实战对比:MVVM + 登录页面(两种实现)

用最经典的登录场景,直观感受两种方案的编码差异。

核心需求

  • 账号 / 密码输入 → 实时校验按钮是否可点击;
  • 点击登录 → 触发网络请求 → 响应结果;
  • 严格遵循 MVVM:ViewModel 无 UIKit,View 仅绑定。

方案 1:MVVM + RxSwift

swift

// ViewModel (无UIKit依赖)
import RxSwift
import RxCocoa

class LoginViewModel {
    // 输入:账号、密码
    let account = BehaviorSubject<String>(value: "")
    let password = BehaviorSubject<String>(value: "")
    // 输出:登录按钮可点击、登录结果
    let isLoginEnabled = Observable<Bool>
    let loginResult = PublishSubject<Bool>()
    
    private let disposeBag = DisposeBag()
    
    init() {
        // 数据流绑定:实时校验输入
        isLoginEnabled = Observable.combineLatest(account, password)
            .map { account, pwd in
                return account.count >= 6 && pwd.count >= 6
            }
        
        // 业务逻辑:登录方法
        func login() {
            // 模拟网络请求
            Observable.just(true)
                .delay(.seconds(1), scheduler: ConcurrentDispatchQueueScheduler(qos: .default))
                .observe(on: MainScheduler.instance)
                .subscribe(onNext: { [weak self] result in
                    self?.loginResult.onNext(result)
                })
                .disposed(by: disposeBag)
        }
    }
}

// View (ViewController)
import UIKit
import RxSwift
import RxCocoa

class LoginVC: UIViewController {
    @IBOutlet weak var accountTF: UITextField!
    @IBOutlet weak var passwordTF: UITextField!
    @IBOutlet weak var loginBtn: UIButton!
    private let vm = LoginViewModel()
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
    }
    
    private func bindViewModel() {
        // 1. UI输入 → ViewModel
        accountTF.rx.text.orEmpty.bind(to: vm.account).disposed(by: disposeBag)
        passwordTF.rx.text.orEmpty.bind(to: vm.password).disposed(by: disposeBag)
        
        // 2. ViewModel状态 → UI渲染
        vm.isLoginEnabled.bind(to: loginBtn.rx.isEnabled).disposed(by: disposeBag)
        
        // 3. UI交互 → ViewModel逻辑
        loginBtn.rx.tap.subscribe(onNext: { [weak self] in
            self?.vm.login()
        }).disposed(by: disposeBag)
        
        // 4. 业务结果 → UI响应
        vm.loginResult.subscribe(onNext: { success in
            print("登录结果:(success)")
        }).disposed(by: disposeBag)
    }
}

方案 2:MVVM + Combine

swift

// ViewModel (无UIKit依赖)
import Combine

class LoginViewModel {
    // 输入:@Published 极简声明
    @Published var account = ""
    @Published var password = ""
    // 输出
    @Published var isLoginEnabled = false
    let loginResult = PassthroughSubject<Bool, Never>()
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 实时校验
        $account.combineLatest($password)
            .map { account, pwd in
                account.count >= 6 && pwd.count >= 6
            }
            .assign(to: &$isLoginEnabled)
    }
    
    func login() {
        // 模拟网络请求 + 异步
        Future<Bool, Never> { promise in
            DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
                promise(.success(true))
            }
        }
        .receive(on: DispatchQueue.main)
        .sink { [weak self] success in
            self?.loginResult.send(success)
        }
        .store(in: &cancellables)
    }
}

// View (ViewController)
import UIKit
import Combine

class LoginVC: UIViewController {
    @IBOutlet weak var accountTF: UITextField!
    @IBOutlet weak var passwordTF: UITextField!
    @IBOutlet weak var loginBtn: UIButton!
    private let vm = LoginViewModel()
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
    }
    
    private func bindViewModel() {
        // UI输入 → ViewModel
        accountTF.publisher(for: .editingChanged)
            .map { $0.text ?? "" }
            .assign(to: &vm.$account)
        
        passwordTF.publisher(for: .editingChanged)
            .map { $0.text ?? "" }
            .assign(to: &vm.$password)
        
        // ViewModel → UI
        vm.$isLoginEnabled
            .assign(to: .isEnabled, on: loginBtn)
            .store(in: &cancellables)
        
        // 点击事件
        loginBtn.publisher(for: .touchUpInside)
            .sink { [weak self] in
                self?.vm.login()
            }
            .store(in: &cancellables)
        
        // 登录结果
        vm.loginResult
            .sink { success in
                print("登录结果:(success)")
            }
            .store(in: &cancellables)
    }
}

七、资深开发选型决策树

无需盲目追新,工程化落地是第一准则:

  1. 项目最低支持 < iOS13 → 唯一选择:RxSwift
  2. 全新项目 ≥iOS13 / SwiftUI 项目 → 首选:Combine
  3. 存量项目逐步升级 → 混合方案:旧页面保留 RxSwift,新页面用 Combine;
  4. 团队无响应式基础 → 优先:Combine(学习成本低,原生规范);
  5. 重度复杂数据流(电商 / 金融) → 优先:RxSwift(生态完善);
  6. 长期维护、追求苹果原生标准 → 必选:Combine

八、工程化避坑指南(资深实战经验)

8.1 MVVM 通用误区

  1. ❌ ViewModel 持有 UIKit 对象 → 破坏可测试性,严格禁止;
  2. ❌ ViewModel 过度臃肿 → 拆分 UseCase/Service,单一职责;
  3. ❌ 为了绑定而绑定 → 简单 UI 用原生,复杂数据流用响应式。

8.2 RxSwift 避坑

  • 内存泄漏:必须DisposeBag管理订阅;
  • 冷 / 热 Observable 误用:网络请求用Single,事件用PublishSubject
  • UI 更新必须切MainScheduler

8.3 Combine 避坑

  • 订阅销毁:必须Set<AnyCancellable>存储,否则订阅立即失效;
  • iOS13 存在 APIbug,建议最低支持 iOS14;
  • 缺少操作符时,用async/await补充。

九、总结

  1. MVVM 的核心:不是三层结构,而是数据驱动 UI+UI 与业务彻底解耦,响应式编程是其唯一高效落地方式;
  2. RxSwift:成熟稳定、生态完善、全版本兼容,是存量项目的最优解
  3. Combine:苹果原生、轻量简洁、未来主流,是新项目的标准答案
  4. 资深 iOS 开发的核心能力:不迷信框架,根据项目场景选型,落地可维护、可测试的工程化架构

iOS 开发已进入SwiftUI+Combine+async/await的原生现代化时代,MVVM 作为核心架构,将长期主导中大型项目的设计。


关键点回顾

  1. MVVM 核心:解耦 + 数据驱动,无响应式则无落地价值;
  2. RxSwift:存量项目、低版本兼容、生态为王;
  3. Combine:新项目、原生未来、简洁轻量;
  4. 选型看系统版本+项目阶段+团队成本,不盲目追新。

【RxSwift】Swift 版 ReactiveX,响应式编程优雅处理异步事件流

作者 探索者dx
2026年4月10日 23:20

【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 核心优势

  1. 统一异步模型:网络请求、通知、KVO、Timer、手势统统归为 Observable,一套 API 走天下
  2. 声明式组合:通过 map/filter/flatMap 等操作符链式调用,代码如流水般清晰
  3. 自动资源管理:DisposeBag 机制确保订阅随生命周期自动释放
  4. 丰富的操作符:100+ 操作符覆盖变换、过滤、组合、错误处理、调度等场景
  5. 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(推荐)

// Package.swift
dependencies: [
    .package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.7.0")
]

CocoaPods

pod 'RxSwift', '~> 6.7'
pod 'RxCocoa', '~> 6.7'  # UIKit 扩展
pod 'RxRelay', '~> 6.7'  # Relay 组件

核心概念:Observable 三部曲

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

// 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) }

常用操作符

// 过滤
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 }                   // 表单验证

线程调度

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)

深入层 源码解析、设计思想、扩展定制

核心协议关系

ObservableType (协议)
    ↓
Observable (class)
    ↓ 创建
AnyObserver (观察者抽象)
    ↓ 订阅
Disposable (取消订阅)
    ↓ 持有
DisposeBag (资源回收容器)

Observer 模式实现

// 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 绑定

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 模式

// 每个操作符返回新的 Observable,支持无限链式
observable
    .map { transform($0) }       // 返回 Map<Source>
    .filter { predicate($0) }    // 返回 Filter<Map<Source>>
    .subscribe { ... }           // 返回 Disposable

takeUntil 实现自动取消

// 当 self.deallocated 时自动取消网络请求
networkRequest()
    .takeUntil(self.rx.deallocated)
    .subscribe(onNext: { ... })

深入层 设计思想解析

Producer-Consumer 模式

// 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 为例:

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 不一致。

解决

// ❌ 错误:未持有 Disposable
observable.subscribe { ... }

// ✅ 正确:加入 DisposeBag
private let disposeBag = DisposeBag()
observable.subscribe { ... }
    .disposed(by: disposeBag)

问题 2:UI 更新不在主线程

问题:后台网络请求回调中更新 UI 导致崩溃。

原因:默认情况下 Observable 继承订阅者的线程上下文。

解决

// ❌ 错误:可能在后台线程
networkRequest()
    .subscribe(onNext: { label.text = $0 })

// ✅ 正确:显式切换到主线程
networkRequest()
    .observe(on: MainScheduler.instance)
    .subscribe(onNext: { label.text = $0 })

问题 3:flatMap 与 flatMapLatest 混淆

问题:快速输入搜索关键词,收到旧的请求结果。

原因flatMap 会保留所有内部 Observable,flatMapLatest 会自动取消上一个。

解决

// ❌ 错误:旧请求可能覆盖新结果
searchText.flatMap { searchAPI($0) }

// ✅ 正确:自动取消上一个请求
searchText.flatMapLatest { searchAPI($0) }

问题 4:Subject 发送 completed 后无法复用

问题:PublishSubject 发送 .completed 后,后续订阅收不到事件。

原因:Subject 一旦 terminated,状态不可逆转。

解决

// 方案 1:使用 Relay(不发送 completed)
let relay = PublishRelay<String>()

// 方案 2:重新创建 Subject
func resetSubject() {
    subject = PublishSubject<String>()
}

问题 5:share(replay:) 重复执行副作用

问题:多个订阅者导致网络请求被执行多次。

原因:默认每个订阅者独立触发 Observable 执行。

解决

// ❌ 错误:每个订阅触发一次请求
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。

解决

// ❌ 错误:闭包内强引用
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 库实现互相转换

八、参考资源

官方资源

推荐文章

系列 Demo 仓库


本期互动

小作业

尝试用 RxSwift 实现一个「搜索建议」功能:输入框输入时防抖 300ms,发起网络请求获取建议列表,展示在 UITableView 中,并在评论区贴出你的关键代码片段。

思考题

如果让你从零实现 RxSwift 的 debounce 操作符,你会如何设计?需要考虑哪些边界情况?

读者征集

你在使用 RxSwift 时踩过哪些坑?欢迎评论区分享,优质回答会收录进下一期《踩坑记录》。


📅 本系列每周五晚更新 · 已学习:[✓ Alamofire] [✓ Kingfisher] [✓ Lottie] [✓ MarkdownUI] [✓ SDWebImage] [✓ SnapKit] [✓ ListDiff] [→ RxSwift] [○ Charts]

❌
❌