为什么旧的 NotificationCenter 会"踩坑"
在 Swift Concurrency 时代,即使你把 addObserver 的 queue 设成 .main,只要闭包里调用了 @MainActor 隔离的函数,编译器依旧会甩出警告:
⚠️ Main actor-isolated property 'xxx' can not be referenced from a non-isolated context
根因:
Notification 默认被标记为 nonisolated,它与 @MainActor 之间没有建立任何"隔离约定",编译器无法证明线程安全。
新 API 的基石:两个协议
| 协议 | 作用 | 适用场景 |
|---|---|---|
MainActorMessage |
保证观察回调一定在主线程执行 | 更新 UI、访问 @MainActor 属性 |
AsyncMessage |
允许在任意隔离域异步投递 | 后台处理、跨 actor 通信 |
系统版本要求:iOS / macOS 26+
MainActorMessage 深入拆解
协议定义
swift
public protocol MainActorMessage: SendableMetatype {
associatedtype Subject
static var name: Notification.Name { get }
// 把 Notification 转成当前消息类型
@MainActor static func makeMessage(_ notification: Notification) -> Self?
// 把消息转回 Notification(供 post 时使用)
@MainActor static func makeNotification(_ message: Self) -> Notification
}
关键注解
- 所有协议要求都在
@MainActor隔离域内完成,编译期即保证线程安全 makeMessage允许你从userInfo、object里取出强类型数据,告别 Any? 强转
系统帮你做好的"现成的"消息类型
以 UIApplication.didBecomeActiveNotification 为例,Swift 26 已内置:
swift
@available(iOS 26.0, *)
extension NotificationCenter.MessageIdentifier where Self ==
NotificationCenter.BaseMessageIdentifier<UIApplication.DidBecomeActiveMessage> {
public static var didBecomeActive: NotificationCenter.BaseMessageIdentifier<UIApplication.DidBecomeActiveMessage> { get }
}
DidBecomeActiveMessage 内部实现:
swift
public struct DidBecomeActiveMessage: NotificationCenter.MainActorMessage {
public static var name: Notification.Name { UIApplication.didBecomeActiveNotification }
public typealias Subject = UIApplication
@MainActor
public static func makeMessage(_ notification: Notification) -> DidBecomeActiveMessage? {
// 系统通知不需要额外参数,直接返回空实例即可
return DidBecomeActiveMessage()
}
@MainActor
public static func makeNotification(_ message: DidBecomeActiveMessage) -> Notification {
Notification(name: name)
}
}
观察方式对比
| 旧写法(仍有并发警告) | 新写法(零警告) |
|---|---|
NotificationCenter.default.addObserver(forName: .didBecomeActiveNotification, object: nil, queue: .main, using: { ... }) |
NotificationCenter.default.addObserver(of: UIApplication.self, for: .didBecomeActive) { message in ... } |
完整新代码:
swift
final class AppActiveMonitor {
private var token: NSObjectProtocol?
func startObserving() {
// ✅ 闭包自动在 @MainActor 执行
token = NotificationCenter.default.addObserver(
of: UIApplication.self,
for: .didBecomeActive
) { [weak self] _ in
self?.handleDidBecomeActive()
}
}
@MainActor
private func handleDidBecomeActive() {
print("✅ 已切换到前台,线程:\(Thread.isMainThread)")
}
}
投递端(post)也受 @MainActor 限制
swift
@MainActor
func postDidBecomeActive() {
let message = DidBecomeActiveMessage()
NotificationCenter.default.post(message, subject: UIApplication.shared)
}
由于
post方法本身被标记为@MainActor,系统保证同步投递,即观察闭包会立即在当前主线程执行,与旧 API 的"异步队列投递"行为不同。
迁移时需评估是否会对现有时序产生副作用。
AsyncMessage:脱离主线程的灵活投递
协议定义
swift
public protocol AsyncMessage: SendableMetatype {
associatedtype Subject
static var name: Notification.Name { get }
// 可在任意隔离域调用,支持异步上下文
static func makeMessage(_ notification: Notification) async -> Self?
static func makeNotification(_ message: Self) async -> Notification
}
与 MainActorMessage 的核心差异
- 没有
@MainActor限制 - 观察闭包为
@Sendable async形式,可并发执行 - 投递方
post不要求主线程,异步分发
自定义 AsyncMessage 实战
假设 RocketSim 插件需要广播"最近构建列表已更新":
步骤 1:定义强类型消息
swift
struct RecentBuild {
let appName: String
}
struct RecentBuildsChangedMessage: NotificationCenter.AsyncMessage {
typealias Subject = [RecentBuild] // 把数组本身当 Subject
let recentBuilds: [RecentBuild]
// 从旧的 notification.object 取出数据
static func makeMessage(_ notification: Notification) async -> RecentBuildsChangedMessage? {
guard let builds = notification.object as? [RecentBuild] else { return nil }
return RecentBuildsChangedMessage(recentBuilds: builds)
}
static func makeNotification(_ message: RecentBuildsChangedMessage) async -> Notification {
Notification(name: .recentBuildsChanged, object: message.recentBuilds)
}
}
步骤 2:添加静态成员,提升可读性
swift
extension NotificationCenter.MessageIdentifier where Self ==
NotificationCenter.BaseMessageIdentifier<RecentBuildsChangedMessage> {
static var recentBuildsChanged: NotificationCenter.BaseMessageIdentifier<RecentBuildsChangedMessage> {
.init()
}
}
步骤 3:发送端(可在后台线程)
swift
func fetchLatestBuilds() async {
let builds = await ServerAPI.latestBuilds()
let message = RecentBuildsChangedMessage(recentBuilds: builds)
await NotificationCenter.default.post(message) // 异步投递
}
步骤 4:观察端(支持任意隔离域)
swift
final class BuildListViewModel {
private var token: NSObjectProtocol?
func startObserving() {
token = NotificationCenter.default.addObserver(
of: [RecentBuild].self,
for: .recentBuildsChanged
) { [weak self] message in
// ✅ 闭包为 async @Sendable,可并发执行
await self?.handleNewBuilds(message.recentBuilds)
}
}
@MainActor
private func handleNewBuilds(_ builds: [RecentBuild]) async {
// 回到主线程刷新 UI
self.builds = builds
}
}
知识速查表
| 特性 | MainActorMessage | AsyncMessage |
|---|---|---|
| 回调线程 | 主线程(同步) | 任意(异步) |
| 发送方限制 | @MainActor | 任意隔离域 |
| 观察闭包 | 同步 | async @Sendable |
| 是否强类型 | ✅ | ✅ |
| 是否需要 async 上下文 | ❌ | ✅ |
总结与迁移建议
-
优先使用 MainActorMessage
只要最终需要刷新 UI,就直接选它,编译期强制主线程,再也不用手写
DispatchQueue.main.async。 -
AsyncMessage 适合"纯后台"链路
例如数据库落地、网络日志上报、跨 actor 通信,不会阻塞主线程。
-
逐步替换,而非一刀切
旧通知可以先封装成新消息类型,双轨并行;观察到无异常后再删除旧代码。
-
单元测试更友好
强类型消息让测试断言不再依赖
userInfo魔法字符串,可读性↑ 维护性↑。