Highlight
Instruments 新增了 Hangs 模板,配合 Time Profiler 和 SwiftUI 模板,开发者可以定位主线程卡顿、异步延迟和主线程阻塞三类性能问题,将响应延迟从数百毫秒降到 100 毫秒以下。
核心内容
你的 App 用户点击按钮,界面却卡住半秒才响应。这种体验像按开关后灯泡延迟亮起,立刻打破”真实交互”的幻觉。
Apple 的研究表明,100 毫秒以下的延迟对离散交互(如点击按钮)几乎不可感知;250 毫秒以上用户会明显察觉;超过 500 毫秒就是真正的 hang。Instruments 的 Hangs 模板从 250 毫秒开始标记 micro hangs,500 毫秒以上标记为正式 hang。
(01:52)找到 hang 后,分析通常分三步:在 Instruments 中识别 hang 类型、定位根因、修复并验证。这场演讲聚焦分析环节,演示了三种典型 hang。
主线程忙碌型 Hang
(19:38)演讲用一个背景选择界面做示例。BackgroundThumbnailView 直接在主线程同步加载缩略图:
struct BackgroundThumbnailView: View {
static let thumbnailSize = CGSize(width:128, height:128)
var background: BackyardBackground
var body: some View {
Image(uiImage: background.thumbnail)
}
}
当 Grid 中同时显示大量缩略图时,每个 background.thumbnail 都在主线程执行图片解码和缩放,导致界面卡住。
修复思路是将同步加载改为异步。先用 ProgressView 占位,再通过 .task 在后台加载:
struct BackgroundThumbnailView: View {
static let thumbnailSize = CGSize(width:128, height:128)
var background: BackyardBackground
@State private var image: UIImage?
var body: some View {
if let image {
Image(uiImage: image)
} else {
ProgressView()
.frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center)
.task {
image = await background.thumbnail
}
}
}
}
关键点:
@State private var image保存异步加载后的图片ProgressView()在图片就绪前显示加载指示器.task修饰符自动在后台执行异步代码,完成后刷新 UIbackground.thumbnail从同步属性改为async计算属性,将耗时操作移出主线程
异步延迟型 Hang
(33:40)即使使用了 await,如果后台任务本身耗时过长,用户依然会看到长时间的 ProgressView。这种情况在 Instruments 中表现为:主线程没有忙碌,但 hang 持续时间很长。
解决方法是把 thumbnail 的计算也做成异步:
public var thumbnail: UIImage {
get async {
// compute and cache thumbnail
}
}
这样解码、缩放都在后台完成,主线程只负责显示结果。
主线程阻塞型 Hang
(38:52)最隐蔽的一类。代码看起来用了 await,但后续代码仍然阻塞主线程:
class ColorizingService {
static let shared = ColorizingService()
func colorize(_ grayscaleImage: CGImage) async throws -> CGImage
}
struct ImageTile: View {
var body: some View {
mainContent
.task {
let colorizer = ColorizingService.shared
result = try await colorizer.colorize(image)
}
}
}
问题出在 ColorizingService.shared 是 @MainActor 上的同步属性。虽然 colorize 是 async,但访问 shared 本身会阻塞主线程。更隐蔽的是,即使写成 await ColorizingService.shared.colorize(image),shared 的同步访问仍然发生在 await 之前。
修复方法是避免在主线程访问同步单例。可以将单例引用提前获取,或重构为不依赖主线程的初始化方式。
详细内容
使用 Instruments 分析 Hang
(01:52)在 Xcode 15 中,选择 Product > Profile,然后选择 Hangs 模板。录制 App 运行后,Instruments 会标记出所有超过阈值的 hang 区间。
时间线区域用红色条标记 hang,下方是各线程的活动情况。点击一个 hang 区间,下方详情面板会显示该时间段内各线程的调用栈。
三类 Hang 的识别特征
| Hang 类型 | 主线程状态 | 识别方法 |
|---|---|---|
| 主线程忙碌 | 100% CPU | Time Profiler 显示主线程在执行你的代码 |
| 异步延迟 | 空闲,等待中 | Hang 持续时间长,主线程没有活动 |
| 主线程阻塞 | 空闲但阻塞 | 调用栈停在同步锁或 actor 隔离的代码上 |
Grid vs LazyVGrid 的选择
(20:26)演讲还对比了 Grid 和 LazyVGrid 的性能差异。Grid 会一次性创建所有子视图,而 LazyVGrid 只在可见区域创建。对于大量数据,优先使用 LazyVGrid:
ScrollView {
LazyVGrid(columns: [.init(.adaptive(minimum: BackgroundThumbnailView.thumbnailSize.width))]) {
ForEach(BackyardBackground.allBackgrounds) { background in
BackgroundThumbnailView(background: background)
}
}
}
关键点:
LazyVGrid只在单元格进入可视区域时才创建视图.adaptive(minimum:)自动计算列数,适配不同屏幕宽度- 配合异步加载,可以避免初次显示时的集中卡顿
结合其他 Instruments 模板
(00:55)Hangs 模板告诉你”哪里卡了”,但要知道”为什么卡”,往往需要添加其他模板:
- Time Profiler:查看各线程的 CPU 使用情况
- SwiftUI:分析视图 body 的执行时间
- Disk Activity:检查是否有同步 I/O 阻塞主线程
- Network:确认延迟是否来自网络请求
核心启发
-
做什么:给 App 的每个列表界面添加异步图片加载
-
为什么值得做:用户滑动列表时最敏感的就是图片加载卡顿。用
ProgressView+.task+async属性,可以将图片解码完全移出主线程 -
怎么开始:找到列表中直接同步加载图片的代码,把加载逻辑改为
async计算属性,视图层用.task触发 -
做什么:在 Instruments 中建立”Hangs + Time Profiler”组合模板,定期 profile 发布版本
-
为什么值得做:很多 hang 在开发环境不明显,因为数据量小、设备性能好。录制真实用户场景才能暴露问题
-
怎么开始:Product > Profile,选择 Hangs 模板,录制后点击红色 hang 条,再添加 Time Profiler 查看调用栈
-
做什么:审计代码中的单例模式,检查是否有
@MainActor同步属性被频繁访问 -
为什么值得做:
static let shared配合@MainActor是主线程阻塞的隐藏陷阱。即使后续代码用了await,访问单例本身的同步操作已经阻塞了 -
怎么开始:全局搜索
.shared,检查其 actor 隔离属性。如果是@MainActor,考虑改为非隔离初始化或提前缓存引用 -
做什么:用
LazyVGrid或LazyVStack替换一次性加载大量数据的Grid/VStack -
为什么值得做:列表有 2000 条数据时,
ForEach会创建 2000 个视图。Lazy系列只在可视区域创建,大幅降低初始加载压力 -
怎么开始:检查所有使用
Grid、VStack+ForEach的列表代码,评估数据量是否适合改为 Lazy 版本
关联 Session
- Track down hangs with Xcode and on-device detection — WWDC22 课程,讲解如何在发布前和发布后发现 hang
- Demystify SwiftUI performance — SwiftUI 性能分析,与 Instruments 配合使用
- What’s new in Xcode 15 — Xcode 15 新功能,包括改进的 Instruments 和 Hangs 模板
- Swift concurrency: Behind the scenes — 深入理解 Swift Concurrency 的线程模型,帮助诊断 actor 隔离导致的阻塞
评论
GitHub Issues · utterances