Highlight
Swift 6.2 把并发模型从「默认并发」转向「默认单线程」,新增
InlineArray、Span、Subprocess、Observations等基础库,并让 macros 项目通过预编译 swift-syntax 大幅缩短 clean build 时间。
核心内容
写过一段时间 Swift 6 并发代码的人,多半都被同一类报错困住过。给一个普通的 class 加个 async 方法,编译器立刻警告它跨过了 main actor 边界;想让一个 @MainActor 类型实现某个普通协议,编译器又说协议实现可能跨线程。一个 App 最常见的场景——UI 类型、单例、回调——都被并发安全检查截了一刀。开发者要么到处贴 @MainActor,要么把代码改造成自己也不太理解的样子。
Swift 6.2 把这件事重新想了一遍。新的默认是:async 函数在调用者所在的 actor 上执行;@MainActor 可以作为模块默认推断模式开启;协议实现可以显式标注为 @MainActor Exportable,让 main actor 类型也能合规地遵守协议。一个完全跑在主线程的 App,写起来又像 Swift 5 时代那样直观,但仍然保留了 Swift 6 的数据竞争检查。需要把 CPU 密集任务卸载到后台时,用 @concurrent 显式标注一次,意图清晰、性能可预期。
除了并发,6.2 还重写了很多日常 API:NotificationCenter 给通知赋予具体类型、消除字典取值和强转;新的 Subprocess 包让脚本场景一行 await run(.name("pwd")) 就能拉起进程;Observations 把 @Observable 的状态变化包成 AsyncSequence;Swift Testing 加入 Attachment 与 exit tests。工具链层面,swiftly 1.0 在 macOS 上线,pre-built swift-syntax 让 macros 项目 clean build 时间动辄少几分钟,Explicitly Built Modules 让 LLDB 第一次执行 p/po 不再卡顿。
详细内容
新的 Subprocess 包(09:44)。Foundation Workgroup 把脚本场景下最常见的「拉起子进程」抽出来做成独立 package:
import Subprocess
let swiftPath = FilePath("/usr/bin/swift")
let result = try await run(
.path(swiftPath),
arguments: ["--version"]
)
let swiftVersion = result.standardOutput
关键点:
import Subprocess:来自独立的 swift-subprocess 包,目前 0.1 版本,等待 1.0 前的反馈。.path(swiftPath):用FilePath指定可执行文件的完整路径;如果给字符串则按$PATH查找。try await run(...):返回的result包含 exit status、standardOutput、standardError等信息,和 async/await 模型自然衔接。
NotificationCenter 用上具体类型(11:34)。过去观察键盘通知要传 UIResponder.keyboardWillShowNotification 字符串、从 userInfo 字典里挖 CGRect、还得处理 main actor 隔离。新 API 把这些步骤合并成一行:
import UIKit
@MainActor
class KeyboardObserver {
func registerObserver(screen: UIScreen) {
let center = NotificationCenter.default
let token = center.addObserver(
of: screen,
for: .keyboardWillShow
) { keyboardState in
let startFrame = keyboardState.startFrame
let endFrame = keyboardState.endFrame
self.keyboardWillShow(startFrame: startFrame, endFrame: endFrame)
}
}
func keyboardWillShow(startFrame: CGRect, endFrame: CGRect) {}
}
关键点:
for: .keyboardWillShow:编译期检查UIScreen是否真的会发这个通知,写错通知名直接编译失败。keyboardState.startFrame:payload 是强类型结构体,不再需要字典 key 加as? CGRect。- 通知声明为
NotificationCenter.MainActorMessage,回调直接处于 main actor,无需额外MainActor.assumeIsolated。
Isolated conformances 解决协议跨界问题(33:04)。@MainActor 类型实现普通协议过去会报 “crosses into main actor-isolated code” 错误。Swift 6.2 允许在协议遵守上写 actor 注解:
protocol Exportable {
func export()
}
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
关键点:
@MainActor Exportable:声明这个 conformance 只能在 main actor 上下文里被使用。- 调用方必须自身也运行在 main actor 上,否则编译器报错,把跨界检查从「禁止遵守」放宽成「使用时检查」。
- 配合
@MainActor模块默认推断模式,单线程 App 几乎不再需要手写@MainActor标注。
显式卸载到后台(35:06)。新模型下 async 默认跑在调用者 actor,要切到全局并发执行器需要显式标注:
class PhotoProcessor {
var cachedStickers: [String: Sticker]
func extractSticker(data: Data, with id: String) async -> Sticker {
if let sticker = cachedStickers[id] {
return sticker
}
let sticker = await Self.extractSubject(from: data)
cachedStickers[id] = sticker
return sticker
}
@concurrent
static func extractSubject(from data: Data) async -> Sticker {}
}
关键点:
extractSticker没有@concurrent,所以跑在调用者的 actor 上,缓存读写天然线程安全。@concurrent static func extractSubject:被标记为可并发执行,离开 main actor 跑在全局执行器。- 把 CPU 密集任务的 hop 集中在一处,性能调优时只需要审视少数
@concurrent函数。
Observations 把状态变化变成 AsyncSequence(14:05):
let player = Player(name: "Holly")
let values = Observations {
let score = "\(player.score) points"
let item =
switch player.item {
case .none: "no item"
case .banana: "a banana"
case .star: "a star"
}
return "\(score) and \(item)"
}
player.score += 2
player.item = .banana
for await value in values { print(value) }
关键点:
Observations { ... }:闭包内访问的所有@Observable属性都会被自动追踪。- 同一 tick 里多次写入(
score += 2与item = .banana)会合并成一次事务化更新,避免中间态被消费。 for await value in values:把 UI 状态以AsyncSequence形式驱动,自然接入 SwiftUI 之外的异步循环。
核心启发
1. 在 Xcode build settings 中打开 @MainActor 默认推断模式
为什么值得做:单线程 UI App 是绝大多数业务代码的形态,开启该模式后省下满屏的 @MainActor 标注,代码更接近 Swift 5 的可读性,同时仍保留 Swift 6 数据竞争检查。
怎么开始:在 SwiftPM manifest 里通过 SwiftSettings API 一次性开启,避免逐文件改;新工程直接默认开启。
2. 把性能关键路径上的 Array 替换为 InlineArray
为什么值得做:固定大小的数据(颜色矩阵、查找表、网格状态)用 InlineArray 直接内联存储在栈上,省掉堆分配和引用计数,渲染、解码、信号处理路径上常能拿到可观收益。
怎么开始:先用 Instruments 定位 hot path 的内存分配热点,再逐个替换,配合 Span 提供安全的零拷贝视图。
3. 把 NotificationCenter 观察迁移到具体类型 API
为什么值得做:消除「通知名拼错」「userInfo key 拼错」「main actor 跨界」三类常见 bug,调用点行数减半,类型即文档。
怎么开始:从 UIKit 键盘、应用生命周期等 Apple 已迁移的通知开始;自定义通知用 MainActorMessage / AsyncMessage 协议加上具体 payload 类型。
4. 给 macros 项目接上 pre-built swift-syntax
为什么值得做:CI 上每次 clean build 省几分钟,本地 Xcode 首次构建也明显加速;macro 作者发布 tagged release 后下游用户无需改动即可受益。
怎么开始:升级到 Xcode 26 / Swift 6.2,将 macro 包依赖锁定到 swift-syntax 的 tagged release,验证 build log 中是否命中预编译产物。
5. 用 @concurrent 替代散落的 Task.detached
为什么值得做:把「这段任务要离开当前 actor」的意图写在函数签名上而非调用点,更适合 review,也让性能分析有清晰的边界。
怎么开始:审视现有 Task.detached、DispatchQueue.global().async 调用,能用静态函数表达的就改成 @concurrent,保留必须运行时调度的少数场景。
关联 Session
- Code-along: Elevate an app with Swift concurrency — 跟着代码动手把现有 App 升级到 Swift 6.2 并发模型
- Embracing Swift concurrency — 系统讲解默认单线程、isolated conformance 等设计选择
- Explore Swift performance — 深入 InlineArray、Span 与内存安全的底层机制
- What’s new in Xcode 26 — Explicitly Built Modules、调试改进等工具链变化
- Meet Containerization — Apple 用 Swift 编写的开源容器运行时
评论
GitHub Issues · utterances