Swift & UI 进阶 20m
可视化与优化 Swift 并发
Visualize and optimize Swift concurrency
2022年6月6日
一句话判断
Instruments 终于有了专门的 Swift Concurrency 模板——你可以直观地看到 actor 阻塞、线程池耗尽、continuation 泄露这些以前只能靠猜的并发问题。
这场 Session 讲了什么
Swift 的 async/await 和 actor 模型虽然大大简化了并发编程,但调试并发性能问题仍然是一个黑盒。这个 Session 介绍了 Xcode 14 中 Instruments 的新 Swift Concurrency 模板,让你能以可视化的方式追踪 Swift 并发的运行时行为。
Session 覆盖了几个典型的性能反模式:
- Main actor 阻塞——在 main actor 上执行耗时操作会导致 UI 卡顿,Instruments 现在能精确定位哪个 async 函数在 main actor 上停留了多久。
- Actor 争用——多个任务同时访问同一个 actor 时,某些任务会被挂起等待。Instruments 能展示这些等待时间的分布。
- 线程池耗尽——如果你在 cooperative thread pool 里跑了太多阻塞操作,新的 async 任务就无法被调度执行。
- Continuation 误用——
withCheckedContinuation如果忘记 resume,会导致任务永远挂起,Instruments 能帮你找到这些泄漏点。
值得深挖的点
Cooperative thread pool 的可视化是最大亮点。 在这之前,开发者几乎无法了解 Swift 并发的线程调度情况。你能看到每个时刻有多少线程在运行、有多少任务在排队、某个 actor 的等待队列有多长。这对于诊断”为什么我的 async 代码比同步代码还慢”这类问题非常关键。
Actor isolation 的性能影响。 Session 揭示了一个容易被忽视的问题:跨 actor 调用是有开销的。每次你从一个 actor 调用到另一个 actor,都涉及一次上下文切换。如果你的代码里有频繁的跨 actor 调用(比如在一个 for 循环里反复调用另一个 actor 的方法),性能损失会非常明显。解决方案是批量传递数据,减少跨 actor 边界的次数。
代码片段
// 典型的 actor 争用问题
actor DataCache {
private var cache: [String: Data] = [:]
func get(_ key: String) -> Data? {
cache[key]
}
func set(_ key: String, data: Data) {
cache[key] = data
}
}
// 问题代码:频繁跨 actor 调用
func loadImages(urls: [URL], cache: DataCache) async throws -> [UIImage] {
var images: [UIImage] = []
for url in urls {
// 每次循环都要跨 actor 边界两次(get + set)
if let cached = await cache.get(url.absoluteString) {
images.append(UIImage(data: cached)!)
} else {
let data = try await URLSession.shared.data(from: url).0
await cache.set(url.absoluteString, data: data)
images.append(UIImage(data: data)!)
}
}
return images
}
// 优化方案:减少跨 actor 调用次数
func loadImagesOptimized(urls: [URL], cache: DataCache) async throws -> [UIImage] {
let keys = urls.map { $0.absoluteString }
var cached = await cache.batchGet(keys) // 一次跨 actor 调用
// ... 处理缺失的数据后一次性写入
await cache.batchSet(newData) // 一次跨 actor 调用
}
最佳实践
- 用 Instruments 的 Swift Concurrency 模板做定期检查,不要等到用户报告卡顿才想起性能问题。
- 检查 main actor 上的执行时间:任何超过几毫秒的 async 操作都不应该放在 main actor 上。
- 注意
@MainActor标注的传播性——一个标记了@MainActor的函数调用的其他函数也可能被隐式放到 main actor 上。 - 使用
withCheckedContinuation而不是withUnsafeContinuation,前者会在 debug 模式下帮你检测遗漏的 resume 调用。
还有什么值得关注
- Instruments 的 Swift Concurrency 视图支持按 actor 过滤,可以只看特定 actor 的活动。
- 新增的 “Swift Tasks” instrument 可以看到每个 task 的创建、挂起、恢复和完成时间线。
- 如果你使用了
TaskGroup,Instruments 能展示子任务之间的父子关系和执行顺序。 - Session 建议在 Release 构建中也要做性能分析,因为某些并发开销在 Debug 和 Release 模式下差异很大。
WWDC 2022