省流版(先给结论)
| 场景 | 建议 |
|---|---|
| App 目标(Xcode 26 新建) | 保持默认 MainActor.self ------ UI 代码省心、并发自己显式开 |
| 纯网络/计算 SPM 包 | 别开 ------ 默认无隔离,保持后台并发能力 |
| UI 组件 SPM 包 | 建议开 ------ 反正迟早跑主线程,省得调用方加 @MainActor |
| 祖传大仓库 | 渐进式:先 package 级开,模块解耦后再整体开 |
什么是"默认 Main Actor 隔离"
Xcode 26 + Swift 6.2 新建项目默认给 App Target 加了两行编译设置:
Global Actor Isolation = MainActor.selfApproachable Concurrency = YES
结果:
- 所有没有显式隔离的代码(class/struct/func)自动被看作
@MainActor。 - 除非手动写
nonisolated或@concurrent,否则默认跑主线程。
- 官方示例:默认隔离长啥样
swift
// 新建项目里什么都不写,等价于:
@MainActor
class MyClass {
@MainActor
var counter = 0
@MainActor
func performWork() async { ... }
// 唯一逃生舱
nonisolated func performOtherWork() async { ... }
}
// 自己声明的 actor 不受影响
actor Counter {
var count = 0 // 仍跑在自己隔离域
}
- SPM 包的命运截然不同
| 项目类型 | 默认 isolation | 默认后台线程 |
|---|---|---|
| App Target | MainActor.self |
❌ |
| SPM Package | 未设置(= nil) |
✅ |
手动给 SPM 打开:
swift
// Package.swift
.target(
name: "MyUI",
swiftSettings: [
.defaultIsolation(MainActor.self) // 跟 App 一样
]
)
为什么苹果要"开历史倒车"------把并发默认关掉?
-
并发 ≠ 性能
线程来回切换 也有成本;很多小操作在主线程干反而更快。
-
Swift 5/6.0 默认"全开并发" → 编译器疯狂报 data race,新人直接劝退。
-
历史习惯:UIKit 时代大家默认主线程,只在需要时才
DispatchQoS.userInitiated。 -
新思路:
- 默认顺序执行(主线程)
- 需要并发时显式加
@concurrent或nonisolated------ 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- 于是疯狂加
@MainActor、Sendable、nonisolated,代码膨胀。
✅ 打开默认隔离------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,然后到处打补丁。