RxSwift 中 BehaviorRelay 详解

1 阅读10分钟

BehaviorRelayRxSwift(结合 RxCocoa)中最常用的**「状态容器」组件,隶属于 RxRelay 模块,用于替代 RxSwift 5 中废弃的 Variable。它本质是对 BehaviorSubject 的安全封装,核心作用是持有并实时推送状态值,兼具「可观察序列(Observable)」和「观察者(Observer)」的双重特性**,是 MVVM 架构中实现 View 与 ViewModel 双向绑定、状态管理的核心工具。

一、本质与核心定位

从底层实现来看,BehaviorRelay 内部封装了一个 BehaviorSubject(可通过源码验证,其核心逻辑依赖 _subject: BehaviorSubject<Element> 实现),但屏蔽了 BehaviorSubject 中可能导致序列终止的 onError(_:) onCompleted()方法,确保序列永不终止,始终可接收和推送新值。

其核心定位是**「带状态的响应式中继器」**,可以类比为「可监听的变量」—— 既可以存储当前的状态值,也可以被订阅,当状态值发生变化时,自动将新值推送给所有订阅者,完美解决了「状态存储 + 状态监听」的双重需求,这也是它在 MVVM 中广泛应用的核心原因。

二、核心特性(必掌握)

  1. 必须初始化初始值:与 PublishRelay 不同,BehaviorRelay 初始化时必须传入一个初始值(value 参数),因为它需要时刻持有当前的状态值,不存在「无初始值」的情况,即使没有任何订阅者,其内部也会保存这个初始值。

  2. 持有并暴露当前值:通过 .value 属性可以同步获取当前的状态值(非异步订阅),这是它区别于普通 Observable 的关键特性,方便在非响应式场景中快速获取状态。

  3. 新订阅者接收最新值:任何新的订阅者(subscribe)都会立即收到 BehaviorRelay 当前持有的最新值(包括初始值),之后再接收后续更新的值。

    例如:先通过 accept(_:) 修改值,再订阅,订阅者会先收到修改后的最新值,而非从初始值开始接收。

  1. 永不终止序列:无法手动调用 onErroronCompleted 终止序列,也不会因为异常而终止,只要对象未被销毁,就可以一直接收新值、推送新值,避免了因序列终止导致的订阅失效问题。

  2. 线程安全:内部实现了线程同步机制,可在多线程环境下通过accept(_:)安全修改值,无需手动加锁,避免了多线程操作导致的状态错乱。

  3. 双重角色:既是 Observable(可被订阅,推送值变化),也是 Observer(可通过 accept(_:) 接收新值,更新自身状态),这种双重特性让它可以轻松衔接响应式流和非响应式代码。

三、基本用法(实操步骤)

使用前需导入依赖:import RxSwiftimport RxCocoaBehaviorRelay 位于 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 功能相似的组件有 BehaviorSubjectPublishRelayVariable(已废弃),重点区分前三者,避免误用,具体对比如下:

特性BehaviorRelayBehaviorSubjectPublishRelay
是否需要初始值是(必传)是(必传)否(无初始值)
是否持有当前值是(.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)

七、注意事项与避坑指南

  1. 避免滥用 .value 属性

    .value 是同步获取值,频繁调用可能导致线程安全问题(虽然 relay 本身线程安全,但.value 获取的是「当前瞬间」的值,若此时正有其他线程修改值,可能获取到中间状态);优先使用响应式订阅获取值,仅在非响应式场景(如判断条件)中使用 .value

  1. 数组/字典修改需「先取后更」

    对于数组、字典等引用类型,不能直接修改 .value 的内容(如 listRelay.value.append("橘子")),这种修改不会触发 relay 推送新值;

必须先获取当前值,修改后再通过accept(_:)重新赋值(参考基本用法 3 中的数组修改示例)。

  1. 订阅必须绑定 disposeBag

    所有 subscribebinddrive 操作都必须添加到 disposeBag 中,否则会导致内存泄漏(relay 持有订阅者,订阅者持有页面,页面持有relay,形成循环引用);若页面销毁时未取消订阅,relay 仍会向销毁的页面推送值,可能导致崩溃。

  1. 禁止手动终止序列

    BehaviorRelay 没有 onErroronCompleted 方法,也不能通过其他方式终止序列,若业务场景需要终止序列,应使用 BehaviorSubject 而非 BehaviorRelay

  1. 避免多线程同时修改值

    虽然 BehaviorRelay 本身线程安全,但频繁在多线程中调用 accept(_:) 会导致值推送顺序混乱,建议统一在主线程或指定线程修改值(可通过 observeOn 切换线程)。

  1. 对外暴露只读 Observable

    ViewModel 中的 BehaviorRelay 应设为私有(如 _goodsList),对外仅暴露 asObservable() 后的只读序列,防止外部直接调用 accept(_:) 修改值,保证状态修改的单一入口,符合「单一职责原则」。

  1. 注意 .value 与订阅回调的同步性

    极少数情况下,调用 accept(_:) 后立即获取 .value,可能出现.value未及时更新的情况(因 accept 内部异步推送值,但.value是同步获取)。

    若需确保获取最新值,可在 subscribe(onNext:) 中获取,而非直接调用 .value

八、总结

BehaviorRelay 是 RxSwift 状态管理的「首选组件」,其核心优势是**「安全、简洁、易用」**—— 封装了 BehaviorSubject 的核心功能,屏蔽了终止序列的风险,同时提供了同步获取值的方式,完美衔接响应式与非响应式代码。

掌握它的关键是:记住「必传初始值、持有当前值、新订阅者收最新值、永不终止」四大特性,区分它与 BehaviorSubjectPublishRelay 的差异,在 MVVM 中遵循**「私有 relay + 对外只读 Observable」**的规范,避免滥用 .value 和多线程修改值,就能轻松应对绝大多数状态管理场景,写出简洁、健壮的响应式代码。