关于 debugging-instruments
面向 Apple 平台开发者的调试与 Instruments 技能指南,涵盖 LLDB、内存、卡顿和构建失败排查。还介绍性能分析工具选择及使用 xctrace 进行命令行采样。
主要功能
- 01LLDB 断点与状态检查
- 02内存图和泄漏排查
- 03主线程卡顿诊断
- 04编译与依赖故障排查
- 05Instruments 模板选择
- 06xctrace 命令行采样
适用场景
- 01定位崩溃或异常状态
- 02调查泄漏和内存持续增长
- 03分析交互卡顿和性能热点
- 04排查构建失败及性能回归
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill debugging-instruments安装后在 Claude Code 中输入/debugging-instruments确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/instruments-guide.mdlldb-patterns.md详细介绍
调试与 Instruments
在这里记录交互式图表和 Instruments 分析。详细的 .memgraph
命令行所有权/增长分析和 ETTrace 工作请转到对应的专用技能。
目录
LLDB 调试
从一套简洁、可重复的工作流程开始:
- 在 Debug 构建中重现问题,并在范围最小且有用的位置设置断点。
- 不执行代码,检查局部变量,然后捕获当前调用栈。
- 转到相关帧或线程,确认故障状态。
- 只有在问题状态的转换仍不明确时,才添加条件或监视点。
(lldb) br set -f ViewModel.swift -l 42 # Stop at file and line
(lldb) v myLocal # Inspect without executing code
(lldb) po myObject # Use debugDescription when needed
(lldb) bt all # Capture every thread's backtrace
(lldb) frame select 3 # Inspect a relevant frame
(lldb) br modify 1 -c "count > 10" # Narrow a noisy breakpoint
(lldb) w set v self.score # Stop on an unexpected write
只需查看局部变量的值时,使用 v 而不是 po——它不会执行代码,也不会触发副作用。表达式求值可能会执行代码或修改程序状态,而硬件监视点数量有限,因此应谨慎使用这两者。
加载 references/lldb-patterns.md,查看完整的检查、断点/日志点、表达式、监视点、线程导航和符号断点命令表。
内存调试
Memory Graph Debugger 工作流程
- 在 Debug 配置中运行应用。
- 重现疑似泄漏的问题(导航到某个屏幕,然后返回)。
- 点击 Xcode 调试栏中的 Memory Graph 按钮。
- 查找紫色警告图标——它们表示存在泄漏对象。
- 选择泄漏对象,查看其引用图和回溯。
运行前启用 Malloc Stack Logging(Scheme > Diagnostics),以便 Memory Graph 显示分配回溯。
常见的循环引用模式
闭包强引用 self:
// LEAK — closure holds strong reference to self
class ProfileViewModel {
var onUpdate: (() -> Void)?
func startObserving() {
onUpdate = {
self.refresh() // strong capture of self
}
}
}
// FIXED — use [weak self]
func startObserving() {
onUpdate = { [weak self] in
self?.refresh()
}
}
代理强引用:
// LEAK — strong delegate creates a cycle
protocol DataDelegate: AnyObject {
func didUpdate()
}
class DataManager {
var delegate: DataDelegate? // should be weak
}
// FIXED — weak delegate
class DataManager {
weak var delegate: DataDelegate?
}
计时器持有目标对象:
// LEAK — Timer.scheduledTimer retains its target
timer = Timer.scheduledTimer(
timeInterval: 1.0, target: self,
selector: #selector(tick), userInfo: nil, repeats: true
)
// FIXED — use closure-based API with [weak self]
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}
Instruments:Allocations 和 Leaks
- Allocations 模板:跟踪内存随时间的增长。使用 “Mark Generation”功能,隔离用户操作期间(例如打开/关闭屏幕)创建的分配。
- Leaks 模板:检测泄漏的分配,包括进程已无法访问的独立循环引用。与 Allocations 同时运行,以获得完整视图。
- 按应用的模块名称筛选,以排除系统分配。
对于泄漏或内存增长分析,可组合使用这些工具:在重现步骤前后使用 Allocations 的 Mark Generation,确认保留内存的增长;然后使用 Memory Graph Debugger 检查对象所有权,并通过 Malloc Stack Logging 获取分配调用栈。
Malloc Stack Logging
在 Scheme > Run > Diagnostics > Malloc Stack Logging 中启用。此功能会记录分配回溯,以便 Memory Graph Debugger、Allocations 工具和导出的 .memgraph 文件显示对象的创建位置。
# Inspect an exported memory graph from Xcode or Instruments
leaks MyApp.memgraph
卡顿诊断
识别主线程卡顿
对于离散交互,低于 100 ms 的延迟通常难以察觉。Apple 开发者工具通常会报告超过 250 ms 的主运行循环繁忙时段,但该报告阈值并非产品目标:几百毫秒的延迟仍可能让人感觉应用无响应。常用检测工具:
- Thread Checker(Xcode Diagnostics):对非主线程调用 UI 发出警告
- Thread Performance Checker:调试时报告优先级反转
- 设备端卡顿检测:Developer Settings 会报告设备使用期间发生的卡顿
- Time Profiler / CPU Profiler / Hitches:分析可重现的卡顿
- os_signpost 和
OSSignposter:标记时间段,供 Instruments 使用 - MetricKit 卡顿诊断:检测生产环境中的卡顿(有关
HangDiagnostic和 iOS 26 兼容性,请参阅metrickit技能)
import os
let signposter = OSSignposter(subsystem: "com.example.app", category: "DataLoad")
func loadData() async {
let state = signposter.beginInterval("loadData")
let result = await fetchFromNetwork()
signposter.endInterval("loadData", state)
process(result)
}
使用 Time Profiler
- 选择 Product > Profile(Cmd+I)以启动 Instruments。
- 选择 Time Profiler 模板。
- 重现卡顿操作,同时录制。
- 重点查看主线程——按“Weight”排序,找到热点路径。
- 勾选“Hide System Libraries”,仅查看自己的代码。
- 双击耗时较高的栈帧,跳转到源代码。
常见卡顿原因
| 原因 | 症状 | 修复方法 |
|---|---|---|
| 在主线程上执行同步 I/O | 网络/文件读取会阻塞 UI | 移至 Task { } 或后台 actor |
| 锁竞争 | 主线程等待后台任务持有的锁 | 使用 actors 或缩小锁的作用范围 |
| 布局抖动 | 反复调用 layoutSubviews | 批量处理布局更改,避免强制布局 |
| 解析大型 JSON 负载 | 加载数据时 UI 卡顿 | 在后台线程解析 |
| 同步解码图像 | 图像较多的列表滚动卡顿 | 使用 AsyncImage 或在主线程之外解码 |
构建失败排查
阅读编译器诊断信息
- 从第一个错误开始查看——后续错误往往是连锁错误。
- 在构建日志中搜索错误代码(例如,
error: cannot convert)。 - 使用 Report Navigator(Cmd+9)查看包含时间戳的完整构建日志。
SPM 依赖解析
# Common: version conflict
error: Dependencies could not be resolved because root depends on 'Package' 1.0.0..<2.0.0
# Fix: check Package.resolved and update version ranges
# Reset package caches if needed:
rm -rf ~/Library/Caches/org.swift.swiftpm
rm -rf .build
swift package resolve
找不到模块/链接器错误
| 错误 | 检查项 |
|---|---|
No such module 'Foo' | 目标成员资格、导入路径、框架搜索路径 |
Undefined symbol | 链接阶段缺少框架、架构错误 |
duplicate symbol | 两个目标定义了相同符号;检查 ObjC 命名冲突 |
优先检查以下构建设置:
FRAMEWORK_SEARCH_PATHSOTHER_LDFLAGSSWIFT_INCLUDE_PATHSBUILD_LIBRARY_FOR_DISTRIBUTION(用于 XCFrameworks)
Instruments 概览
模板选择指南
| 模板 | 适用情况 |
|---|---|
| Time Profiler | CPU 占用高、UI 感觉迟缓,需要查找热点代码路径 |
| Allocations | 内存随时间增长,需要跟踪对象生命周期 |
| Leaks | 怀疑存在保留环或未释放的对象 |
| Network | 检查 HTTP 请求/响应的耗时和负载 |
| SwiftUI | 分析视图 body 的求值和更新频率 |
| Animation Hitches / Core Animation instruments | 帧丢失、卡顿、混合以及提交/渲染工作 |
| Power Profiler | 电池耗电、热压力、后台能耗影响 |
| File Activity | 磁盘 I/O 过多、文件操作缓慢 |
| System Trace | 线程调度、系统调用、虚拟内存故障 |
用于 CI 性能分析的 xctrace CLI
# Record a trace from the command line
xcrun xctrace record --device "My iPhone" \
--template "Time Profiler" \
--instrument "Allocations" \
--output profile.trace \
--launch -- /path/to/MyApp.app
# Export trace data as XML for automated analysis
xcrun xctrace export --input profile.trace --xpath '/trace-toc/run/data/table'
# List available templates
xcrun xctrace list templates
# List connected devices
xcrun xctrace list devices
每次录制使用一个 --template;使用 --instrument 添加其他检测工具。在 CI 流水线中使用 xctrace,自动发现性能回归。比较不同构建导出的指标。
常见错误
不要:使用 print() 调试,而不使用 os.Logger
使用 Logger 设置级别、隐私元数据以及子系统/类别筛选;
.debug 会保留在内存中,且不会持久化到发布版本。
// WRONG — unstructured and not filterable by subsystem/category
print("user tapped button, state: \(viewModel.state)")
print("network response: \(data)")
// CORRECT — structured logging with Logger
import os
let logger = Logger(subsystem: "com.example.app", category: "UI")
logger.debug("Button tapped, state: \(viewModel.state, privacy: .public)")
logger.info("Network response received, bytes: \(data.count)")
不要:在内存调试前忘记启用 Malloc Stack Logging
// WRONG — open Memory Graph without enabling Malloc Stack Logging
// Result: leaked objects visible but no allocation backtrace
// CORRECT — enable BEFORE running:
// Scheme > Run > Diagnostics > check "Malloc Stack Logging: All Allocations"
// Then run, reproduce the leak, and open Memory Graph
不要:调试优化后的代码时期待看到所有变量
// WRONG — profiling with Debug build, debugging with Release build
// Debug builds: extra runtime checks distort perf measurements
// Release builds: variables show as "<optimized out>" in debugger
// CORRECT approach:
// Debugging: use Debug configuration (full symbols, no optimization)
// Profiling: use Release configuration (realistic performance)
不要:在没有条件断点的情况下,每次循环迭代都暂停
// WRONG — breakpoint on line inside loop, stops 10,000 times
for item in items {
process(item) // breakpoint here stops on EVERY item
}
// CORRECT — use a conditional breakpoint:
// (lldb) br set -f MyFile.swift -l 42 -c "item.id == targetID"
// Or in Xcode: right-click breakpoint > Edit > add Condition
不要:忽略 Thread Sanitizer 警告
Thread Sanitizer(TSan)警告表示存在数据竞争,可能只会间歇性导致崩溃。除非已确认是工具问题,否则应将其视为真实错误。
// WRONG — ignoring TSan warning about concurrent access
var cache: [String: Data] = [:] // accessed from multiple threads
// CORRECT — protect shared mutable state
actor CacheActor {
var cache: [String: Data] = [:]
func get(_ key: String) -> Data? { cache[key] }
func set(_ key: String, _ value: Data) { cache[key] = value }
}
启用 TSan:Scheme > Run > Diagnostics > Thread Sanitizer。对于 iOS、iPadOS、 tvOS、visionOS 和 watchOS 应用,请在 Simulator 中运行 TSan;Apple 文档仅说明 64 位 macOS 应用支持在设备上运行。
审查清单
- 使用
os.Logger而非print()输出诊断信息 - 在内存调试会话开始前启用 Malloc Stack Logging
- 检查关闭或释放流程后的内存图调试器
- 将代理声明为
weak var,以防止循环引用 - 属性中存储的闭包使用
[weak self]捕获列表 - 计时器使用基于闭包的 API,并使用
[weak self] - 在模拟器测试方案中启用线程净化器,以排查竞争问题
- 不在主线程上执行同步 I/O 或繁重计算
- 在 Release 构建上运行 Time Profiler,获取性能基线
- 从构建日志中的第一个错误开始排查构建失败
- 使用
OSSignposter记录自定义性能区间 - 使用条件断点调试循环或集合