BehaviorRelay 是 RxSwift(结合 RxCocoa)中最常用的**「状态容器」组件,隶属于 RxRelay 模块,用于替代 RxSwift 5 中废弃的 Variable。它本质是对 BehaviorSubject 的安全封装,核心作用是持有并实时推送状态值,兼具「可观察序列(Observable)」和「观察者(Observer)」的双重特性**,是 MVVM 架构中实现 View 与 ViewModel 双向绑定、状态管理的核心工具。
一、本质与核心定位
从底层实现来看,BehaviorRelay 内部封装了一个 BehaviorSubject(可通过源码验证,其核心逻辑依赖 _subject: BehaviorSubject<Element> 实现),但屏蔽了 BehaviorSubject 中可能导致序列终止的 onError(_:) 和onCompleted()方法,确保序列永不终止,始终可接收和推送新值。
其核心定位是**「带状态的响应式中继器」**,可以类比为「可监听的变量」—— 既可以存储当前的状态值,也可以被订阅,当状态值发生变化时,自动将新值推送给所有订阅者,完美解决了「状态存储 + 状态监听」的双重需求,这也是它在 MVVM 中广泛应用的核心原因。
二、核心特性(必掌握)
-
必须初始化初始值:与
PublishRelay不同,BehaviorRelay初始化时必须传入一个初始值(value参数),因为它需要时刻持有当前的状态值,不存在「无初始值」的情况,即使没有任何订阅者,其内部也会保存这个初始值。 -
持有并暴露当前值:通过
.value属性可以同步获取当前的状态值(非异步订阅),这是它区别于普通 Observable 的关键特性,方便在非响应式场景中快速获取状态。 -
新订阅者接收最新值:任何新的订阅者(
subscribe)都会立即收到BehaviorRelay当前持有的最新值(包括初始值),之后再接收后续更新的值。
例如:先通过 accept(_:) 修改值,再订阅,订阅者会先收到修改后的最新值,而非从初始值开始接收。
-
永不终止序列:无法手动调用
onError或onCompleted终止序列,也不会因为异常而终止,只要对象未被销毁,就可以一直接收新值、推送新值,避免了因序列终止导致的订阅失效问题。 -
线程安全:内部实现了线程同步机制,可在多线程环境下通过
accept(_:)安全修改值,无需手动加锁,避免了多线程操作导致的状态错乱。 -
双重角色:既是
Observable(可被订阅,推送值变化),也是Observer(可通过accept(_:)接收新值,更新自身状态),这种双重特性让它可以轻松衔接响应式流和非响应式代码。
三、基本用法(实操步骤)
使用前需导入依赖:import RxSwift、import RxCocoa(BehaviorRelay 位于 RxCocoa 中,RxRelay 模块已整合至 RxCocoa),以下是最常用的 4 个操作,结合代码示例说明:
1. 初始化(必传初始值)
import RxSwift
import RxCocoa
// 用于管理订阅生命周期,避免内存泄漏
let disposeBag = DisposeBag()
// 初始化:初始值为 0,泛型指定值的类型为 Int
let countRelay = BehaviorRelay<Int>(value: 0)
// 初始化:初始值为空字符串,类型为 String
let textRelay = BehaviorRelay<String>(value: "")
// 初始化:初始值为数组,类型为 [String]
let listRelay = BehaviorRelay<[String]>(value: ["苹果", "香蕉"])
2. 订阅值变化(响应式监听)
通过 asObservable() 方法将其转为 Observable 后订阅(推荐做法,避免外部直接修改 relay),也可直接订阅,订阅后会立即收到当前最新值:
// 方式1:直接订阅(简单场景)
countRelay.subscribe(onNext: { currentCount in
print("当前计数:\(currentCount)") // 首次订阅会打印初始值 0
})
.disposed(by: disposeBag)
// 方式2:转为 Observable 后订阅(推荐,更安全,防止外部误操作)
countRelay.asObservable()
.subscribe(onNext: { currentCount in
print("响应式计数:\(currentCount)")
})
.disposed(by: disposeBag)
3. 修改状态值(accept(_:) 方法)
通过 accept(_:) 方法传入新值,修改 BehaviorRelay 的当前值,同时会自动将新值推送给所有订阅者,这是唯一的修改方式(无其他方法可修改值):
// 修改值:从 0 改为 1,所有订阅者会收到 1
countRelay.accept(1)
// 再次修改:改为 2,订阅者收到 2
countRelay.accept(2)
// 修改数组类型的值(需先获取当前值,修改后再重新赋值)
var currentList = listRelay.value // 同步获取当前数组
currentList.append("橘子") // 修改数组
listRelay.accept(currentList) // 推送新数组,订阅者收到 ["苹果", "香蕉", "橘子"]
4. 同步获取当前值(.value 属性)
通过.value属性可以直接获取当前的状态值,属于同步操作,无需订阅,适合在非响应式场景(如点击按钮时判断当前状态)中使用:
// 同步获取当前计数,此时值为 2
let currentCount = countRelay.value
print("同步获取计数:\(currentCount)") // 输出:2
// 同步获取当前数组,此时值为 ["苹果", "香蕉", "橘子"]
let currentList = listRelay.value
print("同步获取列表:\(currentList)")
四、常用 API 汇总
| API 名称 | 作用 | 使用示例 |
|---|---|---|
| init(value:) | 初始化 BehaviorRelay,必传初始值,指定泛型类型 | BehaviorRelay(value: 0) |
| accept(_:) | 修改当前值,推送新值给所有订阅者,唯一修改值的方法 | countRelay.accept(3) |
| value | 同步获取当前的状态值,非响应式操作 | let count = countRelay.value |
| asObservable() | 将 BehaviorRelay 转为只读 Observable,防止外部误修改 | countRelay.asObservable().subscribe(...) |
| bind(to:) | 将值绑定到其他可观察序列或 UI 控件(如 UILabel 的 text) | countRelay.bind(to: label.rx.text) |
| drive(_:) | 专为 UI 绑定设计,自动切换到主线程,比 bind 更安全(推荐 UI 场景使用) | countRelay.asDriver().drive(label.rx.text) |
五、与相似组件的对比(避坑关键)
RxSwift 中与 BehaviorRelay 功能相似的组件有 BehaviorSubject、PublishRelay、Variable(已废弃),重点区分前三者,避免误用,具体对比如下:
| 特性 | BehaviorRelay | BehaviorSubject | PublishRelay |
|---|---|---|---|
| 是否需要初始值 | 是(必传) | 是(必传) | 否(无初始值) |
| 是否持有当前值 | 是(.value 可获取) | 是(.value 可获取) | 否(无 .value 属性) |
| 新订阅者接收的值 | 最新值(含初始值) | 最新值(含初始值) | 订阅后推送的新值(不接收历史值) |
| 是否可终止序列 | 否(永不终止) | 是(可调用 onError/onCompleted) | 否(永不终止) |
| 修改值的方法 | accept(_:) | onNext(:)、onError(:) 等 | accept(_:) |
| 核心用途 | 状态管理(MVVM 首选) | 临时序列(需手动控制终止) | 事件传递(无状态,如点击事件) |
| 安全性 | 高(屏蔽终止方法,避免误用) | 中(易误调用终止方法) | 高(永不终止,但无状态) |
补充说明:Variable 已在 RxSwift 5 中废弃,其功能完全被 BehaviorRelay 替代,原因是 Variable 内部依赖 BehaviorSubject,但存在线程安全隐患和 API 设计冗余,目前项目中均推荐使用 BehaviorRelay 替代 Variable。
六、实战应用场景(高频)
BehaviorRelay 最核心的场景是「状态管理」,尤其在 MVVM 架构中,以下是 3 个高频实战场景,结合代码片段说明:
场景 1:MVVM 中 ViewModel 的状态存储(核心场景)
ViewModel 中用 BehaviorRelay 存储页面状态(如列表数据、登录状态、输入框文本),View 层订阅这些 relay,实现状态变化自动刷新 UI,ViewModel 内部通过业务逻辑修改 relay 值:
// ViewModel 示例
class HomeViewModel {
// 存储列表数据,初始值为空数组
private let _goodsList = BehaviorRelay<[GoodsModel]>(value: [])
// 对外暴露只读的 Observable,供 View 订阅
var goodsList: Observable<[GoodsModel]> {
return _goodsList.asObservable()
}
// 模拟网络请求,获取数据后更新 relay
func fetchGoodsList() {
NetworkManager.fetchGoods { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let list):
self._goodsList.accept(list) // 更新列表状态,View 会自动刷新
case .failure(let error):
print("请求失败:\(error)")
}
}
}
}
// View 层(ViewController)订阅
class HomeViewController: UIViewController {
private let viewModel = HomeViewModel()
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// 订阅列表数据,刷新表格
viewModel.goodsList
.subscribe(onNext: { [weak self] goodsList in
self?.tableView.reloadData(with: goodsList)
})
.disposed(by: disposeBag)
// 触发网络请求,获取数据
viewModel.fetchGoodsList()
}
}
场景 2:UI 控件双向绑定
实现输入框(UITextField/UITextView)与 BehaviorRelay 的双向绑定:输入框文本变化时,自动修改 relay 值;relay 值变化时,自动更新输入框文本,常用在登录、搜索等场景:
// 1. 定义存储输入文本的 relay
let usernameRelay = BehaviorRelay<String>(value: "")
// 2. 双向绑定(输入框 → relay)
usernameTextField.rx.text.orEmpty
.subscribe(onNext: { [weak self] text in
self?.usernameRelay.accept(text) // 输入框变化,更新 relay
})
.disposed(by: disposeBag)
// 3. 双向绑定(relay → 输入框)
usernameRelay.asObservable()
.bind(to: usernameTextField.rx.text)
.disposed(by: disposeBag)
// 补充:简化双向绑定(可自定义操作符 <->,参考 RxSwift 社区扩展)
// usernameTextField.rx.text.orEmpty <-> usernameRelay
场景 3:多页面/组件状态共享
将 BehaviorRelay 封装在单例中,实现多页面、多组件共享同一个状态(如用户登录状态、全局设置),任何地方修改状态,所有订阅该 relay 的页面/组件都会收到通知并更新:
// 单例:全局状态管理
class GlobalStateManager {
static let shared = GlobalStateManager()
private init() {} // 私有初始化,防止外部创建
// 存储用户登录状态,初始值为未登录(nil)
let loginUser = BehaviorRelay<UserModel?>(value: nil)
// 登录方法:修改登录状态
func login(user: UserModel) {
loginUser.accept(user)
}
// 退出登录方法:重置登录状态
func logout() {
loginUser.accept(nil)
}
}
// 页面 1:订阅登录状态,更新 UI
GlobalStateManager.shared.loginUser.asObservable()
.subscribe(onNext: { user in
if let user = user {
print("当前登录用户:\(user.username)")
// 更新页面 UI(显示用户名、隐藏登录按钮)
} else {
print("未登录")
// 更新页面 UI(显示登录按钮、隐藏用户名)
}
})
.disposed(by: disposeBag)
// 页面 2:执行登录操作,修改全局状态
let user = UserModel(username: "test", id: "123")
GlobalStateManager.shared.login(user: user)
七、注意事项与避坑指南
- 避免滥用 .value 属性
.value 是同步获取值,频繁调用可能导致线程安全问题(虽然 relay 本身线程安全,但.value 获取的是「当前瞬间」的值,若此时正有其他线程修改值,可能获取到中间状态);优先使用响应式订阅获取值,仅在非响应式场景(如判断条件)中使用 .value。
- 数组/字典修改需「先取后更」
对于数组、字典等引用类型,不能直接修改 .value 的内容(如 listRelay.value.append("橘子")),这种修改不会触发 relay 推送新值;
必须先获取当前值,修改后再通过accept(_:)重新赋值(参考基本用法 3 中的数组修改示例)。
-
订阅必须绑定 disposeBag
所有 subscribe、bind、drive 操作都必须添加到 disposeBag 中,否则会导致内存泄漏(relay 持有订阅者,订阅者持有页面,页面持有relay,形成循环引用);若页面销毁时未取消订阅,relay 仍会向销毁的页面推送值,可能导致崩溃。
-
禁止手动终止序列
BehaviorRelay 没有 onError、onCompleted 方法,也不能通过其他方式终止序列,若业务场景需要终止序列,应使用 BehaviorSubject 而非 BehaviorRelay。
-
避免多线程同时修改值
虽然 BehaviorRelay 本身线程安全,但频繁在多线程中调用 accept(_:) 会导致值推送顺序混乱,建议统一在主线程或指定线程修改值(可通过 observeOn 切换线程)。
-
对外暴露只读 Observable
ViewModel 中的 BehaviorRelay 应设为私有(如 _goodsList),对外仅暴露 asObservable() 后的只读序列,防止外部直接调用 accept(_:) 修改值,保证状态修改的单一入口,符合「单一职责原则」。
-
注意 .value 与订阅回调的同步性:
极少数情况下,调用 accept(_:) 后立即获取 .value,可能出现.value未及时更新的情况(因 accept 内部异步推送值,但.value是同步获取)。
若需确保获取最新值,可在 subscribe(onNext:) 中获取,而非直接调用 .value。
八、总结
BehaviorRelay 是 RxSwift 状态管理的「首选组件」,其核心优势是**「安全、简洁、易用」**—— 封装了 BehaviorSubject 的核心功能,屏蔽了终止序列的风险,同时提供了同步获取值的方式,完美衔接响应式与非响应式代码。
掌握它的关键是:记住「必传初始值、持有当前值、新订阅者收最新值、永不终止」四大特性,区分它与 BehaviorSubject、PublishRelay 的差异,在 MVVM 中遵循**「私有 relay + 对外只读 Observable」**的规范,避免滥用 .value 和多线程修改值,就能轻松应对绝大多数状态管理场景,写出简洁、健壮的响应式代码。