前言
适用:RxSwift6 + RxCocoa,Swift + UIKit
阅读前提:第1篇Observable生命周期、第2篇RxCocoa UI绑定、第3篇数据流操作符 本篇重点:搞懂Rx里线程切换底层逻辑;分清
subscribeOn和observeOn(90%新手都混淆);UI线程安全、多线程踩坑实战;MVVM项目里调度器最佳实践
Swift开发我们都知道:UI必须在主线程更新,耗时任务丢后台队列。 传统写法:DispatchQueue.global + DispatchQueue.main.async,手动写闭包嵌套,到处写线程跳转。
Rx里,Scheduler就是Rx封装的调度队列,本质是对GCD队列的一层抽象。
一句话核心:Scheduler决定「事件在哪个线程执行」。
subscribeOn:决定订阅发生在哪个线程(上游序列创建、发送事件的线程)observeOn:决定下游接收事件在哪个线程(操作符、订阅回调执行线程)
一、Rx内置常用Scheduler
先认识项目最常用的几个调度器。
1.MainScheduler
主线程,UI绑定专用。对应DispatchQueue.main。
2.ConcurrentDispatchQueueScheduler
对应ConcurrentDispatchQueueScheduler(qos: .default):并发后台队列,耗时计算/网络。
3.SerialDispatchQueueScheduler
SerialDispatchQueueScheduler(qos: .utility):串行后台队列,顺序任务。
4.CurrentThreadScheduler
CurrentThreadScheduler.instance:当前线程(默认调度器,不指定就用它)。
RxCocoa有个特性:UI控件的rx属性(
textField.rx.text/button.rx.tap)默认就在主线程发送事件。
二、subscribeOn
subscribeOn的作用是控制订阅动作所在线程。
subscribeOn 作用:调用subscribe的时候,上游Observable的订阅、事件产生逻辑,跑在指定线程。
关键点:整个上游管道,从源头开始受它影响;写多次subscribeOn,只有第一次生效!
示例代码:
let disposeBag = DisposeBag()
Observable<Int>.create { observer in
print("✅ Observable创建线程:\(Thread.current)")
observer.onNext(1)
return Disposables.create()
}
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.subscribe(onNext: { value in
print("📥 下游收到:\(value),线程:\(Thread.current)")
})
.disposed(by: disposeBag)
输出:
✅ Observable创建线程:<NSThread: ...>{number = 4, name = (null)}
📥 下游收到:1,线程:<NSThread: ...>{number = 4, name = (null)}
解读:
subscribeOn指定后台线程,Observable.create里面的闭包(生产事件)跑到后台- 没有
observeOn,下游订阅回调继续沿用这个后台线程!
踩坑1:多次调用subscribeOn
obs
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.subscribeOn(MainScheduler.instance) // 无效!会被前面的覆盖
规则:整条链路,只有第一个subscribeOn生效,后面全部忽略。
适用场景:网络请求、本地大文件IO、大数据解析,希望序列生产事件的工作丢到后台。
三、observeOn:
控制【下游事件接收】线程(最常用!)
observeOn = 事件流经过这一步之后,后面所有操作符、订阅回调,全部切到指定Scheduler。
observeOn是管道上的阀门,写在哪个位置,就从该位置开始切换线程,可多次使用,每一次都会切换。
改造上面例子,增加observeOn切回主线:
let disposeBag = DisposeBag()
Observable<Int>.create { observer in
print("✅ Observable创建线程:\(Thread.current)")
observer.onNext(1)
return Disposables.create()
}
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) // 上游生产在后台
.observeOn(MainScheduler.instance) // 从这里开始,下游切主线
.subscribe(onNext: { value in
print("📥 下游收到:\(value),线程:\(Thread.current)")
})
.disposed(by: disposeBag)
输出:
✅ Observable创建线程:<NSThread: ...>{number = 4, name = (null)}
📥 下游收到:1,线程:<NSThread: ...>{number = 1, name = main}
多次observeOn演示
obs
.subscribeOn(backgroundScheduler)
.map { v in
print("map1线程:\(Thread.current)") // 后台线程
return v * 2
}
.observeOn(MainScheduler.instance)
.map { v in
print("map2线程:\(Thread.current)") // 主线程
return v + 1
}
.observeOn(backgroundScheduler)
.subscribe(onNext: { _ in
print("订阅回调线程:\(Thread.current)") // 后台
})
结论:observeOn会改变它之后所有代码的执行线程,可以来回切换。 ✅ 口诀:observeOn,管后面;subscribeOn,管源头,只认第一次。
四、实战标准模板
业务标准范式:
【网络请求(后台执行)】subscribeOn(后台) → 【数据解析】 → observeOn(MainScheduler) → 【UI绑定】
func fetchData() -> Observable<[String]> {
Observable.create { ob in
print("网络请求线程:\(Thread.current)")
// 模拟耗时网络
sleep(1)
ob.onNext(["数据1","数据2"])
ob.onCompleted()
return Disposables.create()
}
}
let disposeBag = DisposeBag()
fetchData()
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.observeOn(MainScheduler.instance)
.subscribe(onNext: { list in
// 更新UI,主线程安全
print("UI刷新,线程:\(Thread.current)")
})
.disposed(by: disposeBag)
五、RxCocoa特殊点:Driver 的调度器魔法
项目MVVM经常用Driver,很多人好奇为什么Driver不用手动写observeOn切主线。
Driver 内部封装了两个特性:
- 强制在主线程监听(内置
observeOn(MainScheduler.instance))- 不会发出error(底层基于Relay)
// 把普通Observable转Driver,网络请求转Driver标准写法
api.fetchList()
.asDriver(onErrorJustReturn: [])
.drive(tableView.rx.items)
.disposed(by: disposeBag)
注意:Driver只是下游监听自动主线,上游请求依然需要subscribeOn丢后台! ❌ 错误认知:用Driver就不用管线程了。Driver只管下游,上游耗时任务依然会阻塞主线。
六、高频踩坑合集(项目最容易翻车的点)
坑1:忘记observeOn(MainScheduler),后台线程直接更新UI
崩溃/诡异UI闪烁!网络回调在后台,直接给label赋值,iOS非主线更新UI,偶现崩溃。
记住:只要最终是绑定UI,管道末尾必须切主线(除非用Driver)
坑2:subscribeOn写在flatMapLatest里面
searchText
.flatMapLatest { keyword in
api.search(keyword: keyword)
.subscribeOn(backgroundScheduler) // ✅ 正确写法!
}
.observeOn(MainScheduler.instance)
.subscribe(...)
flatMapLatest每次产生新内部Observable,每个内部序列需要自己设置subscribeOn。写在flatMap外面的subscribeOn只作用于上游searchText流,不会进入flatMap内部,这是一个超级高频坑。
坑3:混淆RxCocoa UI事件线程
button.rx.tap、textField.rx.text 事件本身在主线。 如果你在这个流后面做大量同步计算,会阻塞主线,这时需要在计算操作符前切后台,算完再切回主线。
textField.rx.text.orEmpty
.observeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.map { heavyCalc($0) } // 大量计算,后台跑
.observeOn(MainScheduler.instance)
.subscribe(onNext: { result in
label.text = result
})
.disposed(by: disposeBag)
坑4:Serial/Concurrent调度器选择错误
- 并发:网络、独立解析任务,互不依赖 → ConcurrentDispatchQueueScheduler
- 串行:需要按顺序执行任务,依赖上一步结果 → SerialDispatchQueueScheduler
七、subscribeOn / observeOn 一句话总结表
| API | 作用范围 | 生效次数 | 使用场景 |
|---|---|---|---|
| subscribeOn | Observable源头、上游事件生产 | 整条链路仅第一次生效 | 网络、IO、耗时任务,指定事件产生的线程 |
| observeOn | 该调用位置之后,下游所有操作符+订阅回调 | 可多次调用,每次切换线程 | 切换下游线程,最常用:末尾切主线更新UI |
八、完整工程示例:
我们通过下面搜索+线程安全的例子整合前几篇知识点。
结合操作符 + Scheduler,一个完整搜索链路:
let disposeBag = DisposeBag()
textField.rx.text.orEmpty
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.filter { !$0.isEmpty }
.flatMapLatest { keyword in
Api.shared.search(keyword: keyword)
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.catchErrorJustReturn([])
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { result in
self.dataSource = result
self.tableView.reloadData()
})
.disposed(by: disposeBag)
链路解读:
- 输入事件来自UI,主线;debounce在主线调度
- 关键词过滤后,flatMap发起网络
- 网络请求subscribeOn切后台执行
- 请求完成后observeOn切回主线,刷新tableView
本篇小结
Scheduler本质就是Rx封装的GCD队列。记住两个核心边界:
-
subscribeOn:控制事件从哪生产,源头;整条链只认第一个。多用于把耗时任务丢后台。 -
observeOn:控制事件后续在哪消费,管道阀门;可以多次切换。UI更新必须切主线。