告别并发警告:Swift 6 线程安全通知 MainActorMessage & AsyncMessage 实战指南

为什么旧的 NotificationCenter 会"踩坑"

在 Swift Concurrency 时代,即使你把 addObserverqueue 设成 .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 允许你从 userInfoobject 里取出强类型数据,告别 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 的核心差异

  1. 没有 @MainActor 限制
  2. 观察闭包为 @Sendable async 形式,可并发执行
  3. 投递方 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 上下文

总结与迁移建议

  1. 优先使用 MainActorMessage

    只要最终需要刷新 UI,就直接选它,编译期强制主线程,再也不用手写 DispatchQueue.main.async

  2. AsyncMessage 适合"纯后台"链路

    例如数据库落地、网络日志上报、跨 actor 通信,不会阻塞主线程。

  3. 逐步替换,而非一刀切

    旧通知可以先封装成新消息类型,双轨并行;观察到无异常后再删除旧代码。

  4. 单元测试更友好

    强类型消息让测试断言不再依赖 userInfo 魔法字符串,可读性↑ 维护性↑。

相关推荐
HarderCoder3 小时前
【SwiftUI 任务身份】task(id:) 如何正确响应依赖变化
swift
非专业程序员4 小时前
精读GitHub - swift-markdown-ui
ios·swiftui·swift
5***790020 小时前
Swift进阶
开发语言·ios·swift
大炮走火1 天前
iOS在制作framework时,oc与swift混编的流程及坑点!
开发语言·ios·swift
0***141 天前
Swift资源
开发语言·ios·swift
z***I3941 天前
Swift Tips
开发语言·ios·swift
J***Q2921 天前
Swift Solutions
开发语言·ios·swift
Gavin-Wang1 天前
Swift + CADisplayLink 弱引用代理(Proxy 模式) 里的陷阱
开发语言·ios·swift
非专业程序员2 天前
Rust RefCell 多线程读为什么也panic了?
rust·swift