Swift中使用NotificationCenter监听值变化是否为不良实践?
当前NotificationCenter方案的评估
你现在的实现能满足功能需求,但在每秒1次高频推送、4~6个VC同时消费数据的场景下存在明显短板,不算最优实践:
- 无编译期校验:通知名、
userInfo传参全靠字符串和无类型字典,key写错、类型转错的问题只能在运行时触发,排查成本高 - 内存管理隐患:虽然iOS 9之后系统对NotificationCenter的观察者做了弱引用处理,但高频推送场景下未及时清理的无效观察者仍会产生不必要的性能开销
- 线程不可控:WebSocket回调默认不保证在主线程,如果不在每个观察者的回调里手动切主线程,很容易出现偶现UI刷新错乱、崩溃,重复冗余代码多
- 逻辑割裂:无法从代码结构直观追溯哪些页面在消费WebSocket数据,后续迭代维护时需要全局搜索通知名,很容易遗漏关联逻辑。
为什么delegate、普通依赖注入不适合这个场景
你之前看到的推荐方案本身没有问题,但适用场景不匹配:
- delegate本质是一对一通信模式,要满足多VC同时监听的需求需要额外实现多播代理,原生单delegate根本无法支撑多观察者场景
- 依赖注入适合有明确层级关系的页面传值(比如父页面给跳转的子页面传参),但WebSocket是全局共享的长连接,各页面之间往往没有直接的层级依赖,每个页面初始化时都手动注入WebSocket实例会让初始化逻辑非常冗余。
推荐的规范实现方案
首选:系统自带Combine实现的类型安全状态容器(iOS 13+ 支持,零第三方依赖)
把WebSocket连接、数据解析、线程切换的逻辑全部收敛到一个单例状态管理器中,用系统自带的@Published做状态分发,完全替代NotificationCenter,轻量无负担:
// 先定义你要传输的数据模型 struct WebSocketPushModel: Codable { // 按你的业务需求定义字段 } final class WebSocketStateManager { static let shared = WebSocketStateManager() // 对外只读的最新推送数据,更新时自动通知所有观察者 @Published private(set) var latestData: WebSocketPushModel? private var webSocketTask: URLSessionWebSocketTask? private init() { setupConnection() } private func setupConnection() { // 省略你的WebSocket连接、重连逻辑 // 收到服务端消息时统一处理 webSocketTask?.receive { [weak self] result in guard let self = self else { return } switch result { case .success(let message): // 按你实际的消息格式解析 if case let .data(rawData) = message, let parsedModel = try? JSONDecoder().decode(WebSocketPushModel.self, from: rawData) { // 统一切主线程更新状态,观察者不需要再处理线程问题 DispatchQueue.main.async { self.latestData = parsedModel } } // 继续接收下一条消息 self.setupConnection() case .failure(let error): // 处理断连、重连逻辑 print("ws error: \(error)") break } } } }
在需要消费数据的VC中,只需要几行代码就能完成订阅,不需要手动移除观察者,页面销毁时订阅会自动取消:
import Combine class DataDisplayViewController: UIViewController { private var cancelBag = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() WebSocketStateManager.shared.$latestData .compactMap { $0 } // 过滤空数据 .sink { [weak self] newData in // 回调默认在主线程,直接更新UI即可 self?.renderUI(with: newData) } .store(in: &cancelBag) } private func renderUI(with data: WebSocketPushModel) { // 你的UI更新逻辑 } }
这个方案的优势很明确:
- 全链路编译期类型检查,不会出现传参错误、类型转换错误
- 线程逻辑统一收敛在管理器内部,业务页面不需要重复写主线程切换代码
- 内存自动管理,不会出现野指针、无效观察者的问题
- 所有WebSocket相关逻辑集中在一个类中,后续维护、排查问题效率极高
- 高频数据场景下的分发性能远高于NotificationCenter,不会有额外的系统开销。
兼容低版本方案:自定义轻量多播代理
如果项目最低支持版本低于iOS13,用不了Combine,自己写一个几十行代码的多播代理实现即可,不需要引入任何第三方库:
// 弱引用存储的多播观察者工具 final class MulticastDispatcher<T> { private let observers = NSHashTable<AnyObject>.weakObjects() func addObserver(_ observer: T) { observers.add(observer as AnyObject) } func dispatch(_ handler: (T) -> Void) { observers.allObjects.forEach { observer in guard let target = observer as? T else { return } handler(target) } } } // 定义观察者协议 protocol WebSocketDataObserver: AnyObject { func onWebSocketNewData(_ data: WebSocketPushModel) } // 改造StateManager final class WebSocketStateManager { static let shared = WebSocketStateManager() private let dispatcher = MulticastDispatcher<WebSocketDataObserver>() private(set) var latestData: WebSocketPushModel? func registerObserver(_ observer: WebSocketDataObserver) { dispatcher.addObserver(observer) } // 收到新数据时调用 private func handleNewData(_ data: WebSocketPushModel) { latestData = data DispatchQueue.main.async { [weak self] in self?.dispatcher.dispatch { $0.onWebSocketNewData(data) } } } }
使用时VC只需要遵守WebSocketDataObserver协议,调用registerObserver完成订阅即可,弱引用机制保证不需要手动移除观察者。
NotificationCenter的合理使用场景
NotificationCenter更适合处理完全跨模块、无业务耦合的系统级事件,比如应用前后台切换、系统键盘状态变化、系统字体变更这类场景,业务模块内部的多页面数据通信完全没必要使用。
内容的提问来源于stack exchange,提问作者SadDeveloper
相关产品推荐
相关产品推荐

