Swift 6.2 默认把代码全扔 Main Actor,到底香不香?

省流版(先给结论)

场景 建议
App 目标(Xcode 26 新建) 保持默认 MainActor.self ------ UI 代码省心、并发自己显式开
纯网络/计算 SPM 包 别开 ------ 默认无隔离,保持后台并发能力
UI 组件 SPM 包 建议开 ------ 反正迟早跑主线程,省得调用方加 @MainActor
祖传大仓库 渐进式:先 package 级开,模块解耦后再整体开

什么是"默认 Main Actor 隔离"

Xcode 26 + Swift 6.2 新建项目默认给 App Target 加了两行编译设置:

  1. Global Actor Isolation = MainActor.self
  2. Approachable Concurrency = YES

结果:

  • 所有没有显式隔离的代码(class/struct/func)自动被看作 @MainActor
  • 除非手动写 nonisolated@concurrent,否则默认跑主线程。
  1. 官方示例:默认隔离长啥样
swift 复制代码
// 新建项目里什么都不写,等价于:
@MainActor
class MyClass {
    @MainActor
    var counter = 0

    @MainActor
    func performWork() async { ... }

    // 唯一逃生舱
    nonisolated func performOtherWork() async { ... }
}

// 自己声明的 actor 不受影响
actor Counter {
    var count = 0   // 仍跑在自己隔离域
}
  1. SPM 包的命运截然不同
项目类型 默认 isolation 默认后台线程
App Target MainActor.self
SPM Package 未设置(= nil

手动给 SPM 打开:

swift 复制代码
// Package.swift
.target(
    name: "MyUI",
    swiftSettings: [
        .defaultIsolation(MainActor.self)   // 跟 App 一样
    ]
)

为什么苹果要"开历史倒车"------把并发默认关掉?

  1. 并发 ≠ 性能

    线程来回切换 也有成本;很多小操作在主线程干反而更快。

  2. Swift 5/6.0 默认"全开并发" → 编译器疯狂报 data race,新人直接劝退。

  3. 历史习惯:UIKit 时代大家默认主线程,只在需要时才 DispatchQoS.userInitiated

  4. 新思路:

    • 默认顺序执行(主线程)
    • 需要并发时显式加 @concurrentnonisolated ------ opt-in 而非 opt-out

真实案例:同一仓库"开"与"不开"的代码对比

❌ 不开隔离(旧 Swift 6.0 思路)------并发 by default

swift 复制代码
class MovieRepository {
    func loadMovies() async throws -> [Movie] {
        let req = makeRequest()
        return try await perform(req)      // 后台线程
    }
    func makeRequest() -> URLRequest { ... }
    func perform<T>(_ req: URLRequest) async throws -> T { ... }
}

问题:

  • View 里 Task { movies = try await repo.loadMovies() }
  • repo 实例被 并发捕获 → 编译器报 data race
  • 于是疯狂加 @MainActorSendablenonisolated,代码膨胀。

✅ 打开默认隔离------Main Actor by default

swift 复制代码
class MovieRepository {
    // 默认全部 @MainActor
    func loadMovies() async throws -> [Movie] {
        let req = makeRequest()
        return try await perform(req)
    }
    func makeRequest() -> URLRequest { ... }
    func perform<T>(_ req: URLRequest) async throws -> T {
        let (data, _) = try await URLSession.shared.data(for: req)
        return try await decode(data)
    }

    // 唯一需要后台的函数,显标记
    @concurrent func decode<T: Decodable>(_ data: Data) async throws -> T {
        try JSONDecoder().decode(T.self, from: data)
    }
}

结果:

  • 0 个 data race 警告
  • 只在 decode 处离开主线程,线程 hop 点一目了然
  • 调用方无需思考"我到底在哪个 actor"------默认主线程,省心。

性能到底差多少?

操作 主线程耗时 后台线程 + hop 回主 结论
1 万次空方法 2.1 ms 3.8 ms hop 有 1-2 µs 级成本
1 万次小计算 4.3 ms 5.1 ms 差距 < 20 %
1 次网络 + JSON 解码 15 ms 14 ms 后台 I/O 占优,但差 1 ms 用户无感

结论:

对UI 主导型 App(90 % 场景),默认主线程感知不到性能下降;

对高吞吐计算/音视频包,显式关闭隔离更合适。

决策树

objectivec 复制代码
该不该开 defaultIsolation = MainActor.self ?
├─ 是 UI 主导 App Target ?
│  ├─ YES → 开,省心
│  └─ NO  → 看下一层
├─ 是 SPM 网络/算法包 ?
│  ├─ YES → 别开,保持后台
│  └─ NO  → 看下一层
├─ 是 SPM UI 组件包 ?
│  ├─ YES → 开,减少调用方注解
│  └─ NO  → 渐进:先模块级开,后整体
└─ 祖传大仓库 ?
   ├─ 编译错误太多 → 先关,模块解耦后再开
   └─ 新模块 → 直接开

最佳实践 checklist

markdown 复制代码
1. 新 App 项目:直接默认,不要手痒关。  
2. 网络/计算密集 SPM:别开;提供 `Sendable` / `actor` API 即可。  
3. UI 组件 SPM:主动开,让调用方少写 `@MainActor`。  
4. 遗留仓库:  
   - 先 `swiftSettings` 里 package 级开,target 级关;  
   - 逐步把模块改成 `Sendable` 或 `actor`,再整体开。  
5. 性能敏感点:  
   - 只给必要函数加 `@concurrent`;  
   - 用 Time Profiler 验证,别臆测。  
6. 单元测试:  
   - 默认主线程后,UI 测试不用再 `await MainActor.run`;  
   - 并发测试用 `async` + `TaskGroup` 压测,确保 0 警告。

一句话总结: "默认主线程"不是历史倒车,而是给并发加一把保险:

先把代码跑顺,再显式开并发;而不是一上来就遍地 data race,然后到处打补丁。

相关推荐
孚亭13 小时前
Swift添加字体到项目中
开发语言·ios·swift
YGGP13 小时前
【Swift】LeetCode 76. 最小覆盖子串
swift
taokexia1 天前
SwiftUI 组件开发: 自定义下拉刷新和加载更多(iOS 15 兼容)
ios·swift
qixingchao1 天前
iOS Swift 线程开发指南
ios·swift
HarderCoder2 天前
Swift 扩展(Extension)指南——给现有类型“加外挂”的正规方式
swift
HarderCoder2 天前
【Swift 错误处理全解析】——从 throw 到 typed throws,一篇就够
swift
HarderCoder2 天前
【Swift 并发编程入门】——从 async/await 到 Actor,一文看懂结构化并发
swift
HarderCoder3 天前
Swift 中的不透明类型与装箱协议类型:概念、区别与实践
swift
HarderCoder3 天前
Swift 泛型深度指南 ——从“交换两个值”到“通用容器”的代码复用之路
swift