Swift 6 并发时代,如何优雅地“抢救”你的单例?

为什么单例在 Swift 6 突然"不香了"

旧认知 Swift 6 新现实
static let shared = XXX()随手一写 编译器直接甩出两行血红诊断:1. 非隔离的全局可变状态(nonisolated global shared mutable state)2. 非 Sendable 类型被共享(non-Sendable type may have shared mutable state
只要不加锁就能跑 编译期即可检测数据竞争(data race)
单例 = 全局可变垃圾桶 必须证明"任意并发上下文都安全"

一句话:Swift 6 不禁止单例,但要求你"自证清白"。

两个核心错误 & 一站式修复清单

错误 1:static var / let 本身"非并发安全"

触发条件 编译器原文
任何 static var Static property 'xxx' is not concurrency-safe because it is nonisolated global shared mutable state

✅ 方案 A:把整个类型扔进 @MainActor

swift 复制代码
// 适合 UI 密切类:整盘菜端给主线程
@MainActor
class GamePiece {
    static var power = 100          // 安全,但任何读写都必须在主线程
    func attack() { GamePiece.power += 10 }   // 隐式 @MainActor
}

✅ 方案 B:只隔离静态变量

swift 复制代码
// 保留其他方法可在后台并发
class GamePiece {
    @MainActor
    static var power = 100          // 仅 power 受主线程保护
    
    // 非隔离方法,可在任意队列执行
    nonisolated func description() -> String { "I am a piece" }
}

⚠️ 方案 C:nonisolated(unsafe) ------ 手动关保险箱

swift 复制代码
class GamePiece {
    // 编译器:我不管了,你自己保证单线程访问
    nonisolated(unsafe) static var power = 100
}

使用守则

  1. 仅当你100% 确定访问序列不会并发(例如启动阶段单任务初始化)。
  2. 在 PR 评论里写下"TODO: Swift 6 临时逃生舱,后续改 actor"。
  3. 每版本复查,争取早日删除。

错误 2:实例"不是 Sendable"却被全局共享

触发条件 编译器原文
static let shared = SomeClass()SomeClass 没证明是 Sendable Static property 'shared' is not concurrency-safe because non-'Sendable' type 'SomeClass' may have shared mutable state

✅ 方案 A:让类" immutable + final + Sendable"

swift 复制代码
// 最简单:纯函数式、无状态
final class AuthProvider: Sendable {
    static let shared = AuthProvider()
    private init() {}
    
    // 只允许读计算属性/方法,无 stored var
    func token() -> String? { UserDefaults.standard.string(forKey: "token") }
}

❌ 踩坑:想偷偷加 mutable 存储属性

swift 复制代码
final class AuthProvider: Sendable {
    static let shared = AuthProvider()
    private var currentToken: String?   // 🚨 Stored property 'currentToken' of 'Sendable'-conforming class is mutable
    private init() {}
}

结论:Sendable class 里一根毛都不能 mutable,否则编译器直接拍桌子。

✅ 方案 B:改 actor ------ 官方推荐终极形态

swift 复制代码
actor AuthProvider {
    static let shared = AuthProvider()   // actor 隐式 Sendable,编译器秒过
    private var currentToken: String?
    
    func update(token: String) {
        currentToken = token
    }
    
    func token() -> String? {
        return currentToken
    }
}

// 使用方
Task {
    await AuthProvider.shared.update(token: "abc")
    let t = await AuthProvider.shared.token()
}

优点

  • 内部自由读写可变状态,外部通过 await 串行排队,零数据竞争。
  • 无需自己加锁、无需 dispatch queue。

缺点

  • 全 async/await 化,老工程需要一层适配。

✅ 方案 C:@unchecked Sendable ------ 老代码逃生舱 2 号

swift 复制代码
// 你已经用 GCD/锁保证线程安全,只是不想大改
final class AuthProvider: @unchecked Sendable {
    static let shared = AuthProvider()
    private var currentToken: String?
    private let lock = NSLock()
    
    func update(token: String) {
        lock.lock()
        currentToken = token
        lock.unlock()
    }
}

守则

  • 写成文档:"本类型已手动保证线程安全,原因如下......"
  • 单元测试必加多线程压力测试,防止未来改代码时破功。
  • 计划 一两个版本内 迁移到 actor。

完整决策树

csharp 复制代码
遇到 static 报错
├─ 是 var ?
│  ├─ 必须可变 → 选隔离策略
│  │  ├─ 可放主线程 → @MainActor
│  │  └─ 不能放主线程 → 改 actor 或 nonisolated(unsafe)
│  └─ 其实可 let → 直接改 let
└─ 是 let 但实例非 Sendable ?
   ├─ 实例无状态 → final class: Sendable
   ├─ 实例有状态但想简单 → actor
   └─ 有状态且暂时不改 → @unchecked Sendable + 手动锁

总结与实战感受

  1. Swift 6 把"并发安全"从君子协定变成编译器红线。

    以前"跑不死就行"的代码,现在不证明安全就编译不过------这是好事,越早暴露问题,线上越少崩溃。

  2. actor 是苹果官方给出的"单例+可变状态"黄金搭档。

    只要你的业务层已经拥抱 async/await,把单例改成 actor 通常改动量比加锁小,且可维护性更高。

  3. @MainActor 是把双刃剑

    • 适合真正 UI 绑定类(如 ThemeManagerNavigationRouter)。
    • 别因为"编译不过"就一股脑扔主线程,否则会把主线程拖成单线程瓶颈。
  4. nonisolated(unsafe) 与 @unchecked Sendable 只能是"技术债",

    务必记录 TODO、写压力测试、排进迭代计划,否则"临时"会变"永久",日后数据竞争哭都来不及。

  5. 单元测试必须多线程跑:

    即使编译器绿了,也写个 1000_task_simultaneously_read_write 测试,用 XCTAssert 锁死预期行为------这是最后一道保险。

扩展场景:更复杂的单例形态怎么玩?

  1. 需要后台刷新的缓存池

    • actor + Task { await backgroundRefresh() }
    • 通过 nonisolated 暴露只读接口,让外部无需 await 即可读取快照。
  2. 多隔离域协作的"混合单例"

    • 将"读"放任意线程(nonisolated 计算属性)。
    • 将"写"集中到自定义全局 actor(@globalActor),避免主线程被占用。
  3. 依赖注入容器(DI Container)

    • actor DIContainer 管理所有服务注册表,注册/解析全程线程安全。
    • 配合 @propertyWrapper 实现 Injected,在 SwiftUI/Redux 架构里无痛取单例。
  4. 与 Objective-C 共舞的老库

    • .mm 文件用 std::mutex 保证 C++ 可变数据安全,然后 Swift 侧包一层 @unchecked Sendable 壳。
    • 记得把锁粒度降到"临界区"最小,防止性能回退。

一句忠告

单例不是原罪,全局可变状态才是。Swift 6 只是强迫我们"把状态关进笼子里"。

选对隔离策略、写好测试、及时还技术债,你的单例依旧可以优雅地活到下一个大版本。祝大家编译全绿,崩溃为零!

相关推荐
zhangmeng3 小时前
FlutterBoost在iOS26真机运行崩溃问题
flutter·app·swift
HarderCoder3 小时前
SwiftUI 踩坑记:onAppear / task 不回调?90% 撞上了“空壳视图”!
swift
HarderCoder3 小时前
@isolated(any) 深度解析:Swift 并发中的“隔离追踪器”
swift
大熊猫侯佩7 小时前
桃花岛 Xcode 构建秘籍:Swift 中的 “Feature Flags” 心法
app·xcode·swift
用户097 小时前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan7 小时前
iOS26适配指南之UIColor
ios·swift
HarderCoder9 小时前
Swift 6.2 新特性 `@concurrent` 完全导读
swift
HarderCoder9 小时前
Swift 里的“橡皮擦”与“标签”——搞懂 existentials 与 primary associated type
swift
用户091 天前
TipKit与CloudKit同步完全指南
ios·swift