第4篇 | Scheduler调度器 —— Rx线程模型,subscribeOn / observeOn 彻底拆解

前言

适用: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 内部封装了两个特性:

  1. 强制在主线程监听(内置observeOn(MainScheduler.instance))
  2. 不会发出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)

链路解读:

  1. 输入事件来自UI,主线;debounce在主线调度
  2. 关键词过滤后,flatMap发起网络
  3. 网络请求subscribeOn切后台执行
  4. 请求完成后observeOn切回主线,刷新tableView

本篇小结

Scheduler本质就是Rx封装的GCD队列。记住两个核心边界:

  1. subscribeOn:控制事件从哪生产,源头;整条链只认第一个。多用于把耗时任务丢后台。

  2. observeOn:控制事件后续在哪消费,管道阀门;可以多次切换。UI更新必须切主线。

相关推荐
sweet丶3 个月前
Swift 黑魔法:`@dynamicMemberLookup`
swift·rxswift
择势6 个月前
MVVM 本质解构 + RxSwift 与 Combine 深度对决与选型指南
swiftui·swift·rxswift
用户79457223954136 个月前
【RxSwift】Swift 版 ReactiveX,响应式编程优雅处理异步事件流
swift·rxswift
符哥20088 个月前
使用Apollo和GraphQL搭建一套网络框架
ios·swift·rxswift
UVhsSauK8 个月前
MATLAB代码 分布式电源/储能选址定容 可任意修改接入分布式电源数目 以IEEE33配电网...
rxswift
符哥20088 个月前
用Apollo + RxSwift + RxCocoa搭建一套网络请求框架
网络·ios·rxswift
vftOWpVs9 个月前
使用粒子群算法PSO优化最小支持向量机LSSVM实现分类
rxswift
hid6466372210 个月前
混合A星泊车路径规划系统:内含代码、参考文献与思维导图设计,包含RS曲线与碰撞检测解析
rxswift
聊询QQ:6882388610 个月前
用LightGBM解决多元回归与时间序列问题:5折交叉验证与Hyperopt超参数优化实战
rxswift