使用 LLDB、交互式内存图调试器和 Instruments 调试 iOS 应用并分析性能。适用于崩溃、循环引用检查、卡顿、构建失败以及通用的 CPU、内存、能耗或网络分析。如需 .memgraph 捕获、leaks CLI 所有权路径或持久堆增长,请使用 ios-memgraph-analysis;如需 ETTrace 捕获和 JSON,请使用 ios-ettrace-performance。
调试与 Instruments
将交互式图形和 Instruments 分类处理放在此处。将详细的 .memgraph 命令行所有权/增长分析和 ETTrace 工作路由到各自的专项技能。
目录
LLDB 调试
从一个可重复的小工作流开始:
- 在 Debug 构建中复现,并在最窄的有用断点处停止。
- 在不执行代码的情况下检查局部变量,然后捕获当前堆栈。
- 移动到相关帧或线程,验证失败状态。
- 仅在错误转换仍不明确时添加条件或监视点。
(lldb) br set -f ViewModel.swift -l 42 # 在文件和行处停止
(lldb) v myLocal # 不执行代码检查
(lldb) po myObject # 需要时使用 debugDescription
(lldb) bt all # 捕获所有线程的回溯
(lldb) frame select 3 # 检查相关帧
(lldb) br modify 1 -c "count > 10" # 缩小嘈杂断点范围
(lldb) w set v self.score # 在意外写入时停止
当只需要局部变量值时,优先使用 v 而非 po——它不执行代码,不会触发副作用。表达式求值可能执行或改变程序状态,而硬件监视点数量有限,因此请谨慎使用两者。
加载 references/lldb-patterns.md 获取完整的检查、断点/日志点、表达式、监视点、线程导航和符号断点命令表。
内存调试
内存图调试器工作流
- 在 Debug 配置下运行应用。
- 复现疑似泄漏(导航到某个屏幕,然后返回)。
- 点击 Xcode 调试栏中的内存图按钮。
- 查找紫色警告图标——这些表示泄漏的对象。
- 选择一个泄漏对象以查看其引用图和回溯。
在运行前启用 Malloc Stack Logging(Scheme > Diagnostics),以便内存图显示分配回溯。
常见循环引用模式
闭包强捕获 self:
// 泄漏——闭包持有 self 的强引用
class ProfileViewModel {
var onUpdate: (() -> Void)?
func startObserving() {
onUpdate = {
self.refresh() // 强捕获 self
}
}
}
// 修复——使用 [weak self]
func startObserving() {
onUpdate = { [weak self] in
self?.refresh()
}
}
强委托引用:
// 泄漏——强委托造成循环
protocol DataDelegate: AnyObject {
func didUpdate()
}
class DataManager {
var delegate: DataDelegate? // 应为 weak
}
// 修复——弱委托
class DataManager {
weak var delegate: DataDelegate?
}
Timer 保留 target:
// 泄漏——Timer.scheduledTimer 保留其 target
timer = Timer.scheduledTimer(
timeInterval: 1.0, target: self,
selector: #selector(tick), userInfo: nil, repeats: true
)
// 修复——使用基于闭包的 API 配合 [weak self]
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}
Instruments:Allocations 和 Leaks
- Allocations 模板:跟踪内存随时间增长。使用“标记生成”功能隔离用户操作之间(例如打开/关闭屏幕)创建的分配。
- Leaks 模板:检测泄漏的分配,包括进程无法再访问的孤立循环引用。与 Allocations 一起运行以获得完整视图。
- 按应用的模块名称过滤以排除系统分配。
对于泄漏或内存增长分类,配对使用工具:在复现步骤前后使用 Allocations 标记生成来证明保留增长,然后使用内存图调试器检查对象所有权,并使用 Malloc Stack Logging 恢复分配调用堆栈。
Malloc Stack Logging
在 Scheme > Run > Diagnostics > Malloc Stack Logging 中启用。这会记录分配回溯,以便内存图调试器、Allocations 工具和导出的 .memgraph 文件显示对象的创建位置。
# 检查从 Xcode 或 Instruments 导出的内存图
leaks MyApp.memgraph
卡顿诊断
识别主线程卡顿
对于离散交互,100 毫秒以下的延迟通常不易察觉。Apple 开发者工具通常报告主运行循环忙碌时间超过 250 毫秒,但该报告阈值并非产品目标:几百毫秒仍可能感觉无响应。常见检测工具:
- Thread Checker(Xcode Diagnostics):警告非主线程 UI 调用
- Thread Performance Checker:调试时报告优先级反转
- 设备端卡顿检测:开发者设置报告设备使用中的卡顿
- Time Profiler / CPU Profiler / Hitches:分析可复现的卡顿
- os_signpost 和
OSSignposter:标记 Instruments 中的时间间隔 - MetricKit 卡顿诊断:生产环境卡顿检测(参见
metrickit技能了解HangDiagnostic和 iOS 26 兼容性)
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 模板。
- 在复现慢速交互时录制。
- 聚焦主线程——按“权重”排序以查找热点路径。
- 勾选“隐藏系统库”以仅查看你的代码。
- 双击重帧跳转到源代码。
常见卡顿原因
| 原因 | 症状 | 修复 |
|---|---|---|
| 主线程同步 I/O | 网络/文件读取阻塞 UI | 移至 Task { } 或后台 actor |
| 锁竞争 | 主线程等待后台工作持有的锁 | 使用 actor 或减少锁范围 |
| 布局抖动 | 重复调用 layoutSubviews |
批量布局更改,避免强制布局 |
| 解析大型 JSON 负载 | 数据加载时 UI 冻结 | 在后台线程解析 |
| 同步图像解码 | 图片密集列表滚动卡顿 | 使用 AsyncImage 或在主线程外解码 |
构建失败分类
阅读编译器诊断
- 从第一个错误开始——后续错误通常是级联的。
- 在构建日志中搜索错误代码(例如
error: cannot convert)。 - 使用报告导航器(Cmd+9)查看带有时间戳的完整构建日志。
SPM 依赖解析
# 常见:版本冲突
error: Dependencies could not be resolved because root depends on 'Package' 1.0.0..<2.0.0
# 修复:检查 Package.resolved 并更新版本范围
# 如果需要,重置包缓存:
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
# 从命令行录制跟踪
xcrun xctrace record --device "My iPhone" \
--template "Time Profiler" \
--instrument "Allocations" \
--output profile.trace \
--launch -- /path/to/MyApp.app
# 将跟踪数据导出为 XML 以进行自动化分析
xcrun xctrace export --input profile.trace --xpath '/trace-toc/run/data/table'
# 列出可用模板
xcrun xctrace list templates
# 列出连接的设备
xcrun xctrace list devices
每次录制使用一个 --template;使用 --instrument 添加额外工具。在 CI 流水线中使用 xctrace 自动捕获性能回归。比较构建之间导出的指标。
常见错误
不要:使用 print() 调试而不用 os.Logger
使用 Logger 实现级别、隐私元数据和子系统/类别过滤;.debug 保留在内存中,在发布构建中不会持久化。
// 错误——非结构化,无法按子系统/类别过滤
print("user tapped button, state: \(viewModel.state)")
print("network response: \(data)")
// 正确——使用 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
// 错误——打开内存图但未启用 Malloc Stack Logging
// 结果:泄漏对象可见但无分配回溯
// 正确——在运行前启用:
// Scheme > Run > Diagnostics > 勾选 "Malloc Stack Logging: All Allocations"
// 然后运行,复现泄漏,打开内存图
不要:调试优化代码期望完整变量可见性
// 错误——使用 Debug 构建分析,使用 Release 构建调试
// Debug 构建:额外运行时检查扭曲性能测量
// Release 构建:变量在调试器中显示为 "<optimized out>"
// 正确方法:
// 调试:使用 Debug 配置(完整符号,无优化)
// 分析:使用 Release 配置(真实性能)
不要:不使用条件断点就在每个循环迭代上停止
// 错误——在循环内行设置断点,停止 10,000 次
for item in items {
process(item) // 此处断点在每个项目上停止
}
// 正确——使用条件断点:
// (lldb) br set -f MyFile.swift -l 42 -c "item.id == targetID"
// 或在 Xcode 中:右键断点 > 编辑 > 添加条件
不要:忽略线程清理器警告
线程清理器(TSan)警告表明数据竞争,可能仅间歇性崩溃。除非已隔离出工具问题,否则将其视为真实错误。
// 错误——忽略关于并发访问的 TSan 警告
var cache: [String: Data] = [:] // 从多个线程访问
// 正确——保护共享可变状态
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 应用,在模拟器中运行 TSan;Apple 文档仅支持 64 位 macOS 应用的设备支持。
审查清单
- [ ] 使用
os.Logger而非print()进行诊断输出 - [ ] 在内存调试会话前启用 Malloc Stack Logging
- [ ] 在 dismiss/dealloc 流程后检查内存图调试器
- [ ] 委托声明为
weak var以防止循环引用 - [ ] 作为属性存储的闭包使用
[weak self]捕获列表 - [ ] 定时器使用基于闭包的 API 配合
[weak self] - [ ] 在模拟器测试方案中启用线程清理器以进行竞争分类
- [ ] 主线程上无同步 I/O 或繁重计算
- [ ] 在 Release 构建上运行 Time Profiler 以获取性能基线
- [ ] 从构建日志的第一个错误开始分类构建失败
- [ ] 使用
OSSignposter进行自定义性能时间间隔 - [ ] 对循环/集合调试使用条件断点






