Highlight
Xcode 26 新建 App 项目默认开启 Main Actor 模式,所有代码隐式带上
@MainActor,需要后台执行时再用@concurrent显式标注。
核心内容
很多开发者第一次写网络请求的时候都踩过同一个坑:在主线程同步调 URLSession.shared.data(from:),UI 立刻卡死几秒,应用看起来像挂掉了。这是因为主线程既要响应触摸事件、又要刷新 UI,一旦被网络 IO 占住就什么都干不了。Swift 团队这次给出的方案不再是新增一套 GCD API,而是从语言层面把”在哪个线程跑”这件事变成可被编译器检查的属性。
iOS 19 / Xcode 26 的新 App 项目默认开启 Main Actor 模式。这意味着你不写任何标注的代码都隐式带着 @MainActor,编译器知道它只能在主线程跑,于是允许你随便访问全局变量、单例。当某段计算(比如图片解码)开始让 UI 掉帧,你给那个函数加上 @concurrent,编译器会立刻指出它访问了哪些主线程独占的状态,逼你把这些状态用值类型或 actor 包好再传过去。整个迁移路径是渐进的:从单线程开始,先用 async/await 处理高延迟,再用 @concurrent 卸载 CPU 密集任务,最后用 actor 把数据本身搬离主线程。
详细内容
整段演讲围绕一个图片加载的例子展开。最初版本是同步阻塞的:
final class ImageModel {
var imageCache: [URL: Image] = [:]
let view = View()
func fetchAndDisplayImage(url: URL) throws {
let data = try Data(contentsOf: url)
let image = decodeImage(data)
view.displayImage(image)
}
}
关键点:
fetchAndDisplayImage是同步函数,整个流程在主线程一口气跑完。- 本地读小文件没问题,但如果换成网络 URL,主线程会被网络 IO 阻塞,UI 直接冻住。
第一步是把它改成 async,让网络 IO 不再阻塞主线程(06:10):
final class ImageModel {
var imageCache: [URL: Image] = [:]
let view = View()
func fetchAndDisplayImage(url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
let image = decodeImage(data)
view.displayImage(image)
}
}
关键点:
- 函数签名加了
async,调用URLSession时用await。 await标记了挂起点,函数会在这里让出主线程,等数据回来再恢复。- 函数还在主线程上,但主线程在等网络期间可以处理触摸事件、绘制其他 UI。
URLSession的实现在后台线程做实际 IO,应用代码本身没引入并发。
async 函数必须在 task 里启动。点击按钮时这样写(07:31):
func onTapEvent() {
Task {
do {
try await fetchAndDisplayImage(url: url)
} catch let error {
displayError(error)
}
}
}
关键点:
Task { ... }创建一个新任务,包裹整个 fetch + display 流程。- 任务从头到尾按顺序执行,多个任务之间通过 interleaving 在主线程上轮流跑。
- error 在 task 内部 catch,避免吞掉异常。
当应用真的遇到主线程卡顿——比如 decodeImage 处理大图太慢——才需要把这部分搬到后台。这时用 @concurrent(11:11):
final class ImageModel {
var imageCache: [URL: Image] = [:]
let view = View()
func fetchAndDisplayImage(url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url)
let image = await decodeImage(data, at: url)
view.displayImage(image)
}
@concurrent
func decodeImage(_ data: Data, at url: URL) async -> Image {
Image()
}
}
关键点:
@concurrent告诉 Swift 这个函数总是在后台运行。- 调用方用
await等结果,主线程在解码期间可以继续干别的。 - 此时
decodeImage不再处于 main actor 隔离域,编译器会拒绝它直接访问imageCache——这是设计目的,强制你把跨线程共享的数据显式处理。
针对共享可变引用类型的数据竞争问题,演讲给出 actor 方案(25:10):
actor NetworkManager {
var openConnections: [URL: Connection] = [:]
func openConnection(for url: URL) async -> Connection {
if let connection = openConnections[url] {
return connection
}
let connection = Connection()
openConnections[url] = connection
return connection
}
func closeConnection(_ connection: Connection, for url: URL) async {
openConnections.removeValue(forKey: url)
}
}
关键点:
- 把
class改成actor,openConnections的可变状态被 actor 隔离保护。 - 外部调用
openConnection/closeConnection必须await,actor 保证同一时刻只有一个调用在内部执行。 - 主线程和后台线程都能安全使用同一个
NetworkManager实例,无需手动加锁。
值类型走另一条路:struct 和 enum 默认满足 Sendable,跨任务传递时复制语义自动避免共享(16:56)。先用值类型,搞不定共享可变状态再上 actor,是 Apple 推荐的优先级。
核心启发
-
做什么:在新建 iOS 19 项目时确认开启 Main Actor 默认模式
- 为什么值得做:Xcode 26 新模板默认开启,编译器自动给 UI 模块的代码加
@MainActor,省掉一堆显式标注,访问 UI 单例和全局状态不会再被报”data race”。 - 怎么开始:在 Build Settings 里搜 “Default Actor Isolation”,确认是
MainActor。老项目可以从 UI 相关的 module 开始切换,业务逻辑 module 暂时保持原状。
- 为什么值得做:Xcode 26 新模板默认开启,编译器自动给 UI 模块的代码加
-
做什么:把
URLSession.shared.data(...)同步调用全部替换成async/await- 为什么值得做:同步网络 IO 在主线程几乎肯定造成 UI 卡顿。
async版本不需要你引入并发,只要await一下就把卡顿点变成挂起点。 - 怎么开始:搜代码里所有
Data(contentsOf:)和同步的URLSession用法,把调用方改成async函数,用Task { }在按钮回调里启动。
- 为什么值得做:同步网络 IO 在主线程几乎肯定造成 UI 卡顿。
-
做什么:用 Instruments 找出主线程上真正耗时的计算,再决定加
@concurrent- 为什么值得做:演讲明确说”很多 app 不需要并发”。盲目把所有耗时操作搬到后台只会让代码变复杂、引入新的数据竞争问题,应该先用 profiler 定位瓶颈。
- 怎么开始:在 Instruments 里跑 Time Profiler,找到主线程占用最高的函数。如果确实是 CPU 密集(比如图像解码、JSON 解析大数据),给那个函数加
@concurrent,让编译器告诉你它访问了哪些隔离状态。
-
做什么:把跨线程共享的可变引用类型重构为 actor
- 为什么值得做:单例式的
class一旦被多线程访问就需要手动加锁,容易写错。actor把同步原语内置在语言层,外部调用必须await,编译器帮你拦住所有数据竞争。 - 怎么开始:列出项目里被多个线程访问的 manager / cache 类,逐个改成
actor。注意外部调用全部要await,可能需要把调用链上的函数都改成async。
- 为什么值得做:单例式的
-
做什么:用值类型 +
Sendable处理跨任务数据传递- 为什么值得做:
struct默认满足Sendable,复制语义天然避免共享,比 actor 简单得多。Apple 在演讲里把”值类型优先”列为第一原则。 - 怎么开始:检查代码里跨任务传递的数据,能用
struct表达就别用class。引用类型如果一定要传,确认它是不可变的并标Sendable,否则改成值类型快照。
- 为什么值得做:
关联 Session
- Code-along: Cook up a rich text experience in SwiftUI with AttributedString — 在 SwiftUI 里搭建富文本编辑体验,配合 Swift 并发处理数据更新
- Explore Swift and Java interoperability — Swift 与 Java 在同一代码库混用,跨语言调用如何处理并发边界
- Improve memory usage and performance with Swift — Swift 内存管理与性能优化技巧,适合搭配
@concurrent卸载后阅读 - Optimize SwiftUI performance with Instruments — 用新版 SwiftUI Instrument 定位 UI 卡顿,决定是否需要引入并发
评论
GitHub Issues · utterances