debugging-instruments

debugging-instruments

热门

使用 LLDB、交互式内存图调试器和 Instruments 调试 iOS 应用并分析性能。适用于崩溃、循环引用检查、卡顿、构建失败以及通用的 CPU、内存、能耗或网络分析。如需 .memgraph 捕获、leaks CLI 所有权路径或持久堆增长,请使用 ios-memgraph-analysis;如需 ETTrace 捕获和 JSON,请使用 ios-ettrace-performance。

932Star
47Fork
更新于 2026/7/15
SKILL.md
readonly只读
name
debugging-instruments
description

使用 LLDB、交互式内存图调试器和 Instruments 调试 iOS 应用并分析性能。适用于崩溃、循环引用检查、卡顿、构建失败以及通用的 CPU、内存、能耗或网络分析。如需 .memgraph 捕获、leaks CLI 所有权路径或持久堆增长,请使用 ios-memgraph-analysis;如需 ETTrace 捕获和 JSON,请使用 ios-ettrace-performance。

调试与 Instruments

将交互式图形和 Instruments 分类处理放在此处。将详细的 .memgraph 命令行所有权/增长分析和 ETTrace 工作路由到各自的专项技能。

目录

LLDB 调试

从一个可重复的小工作流开始:

  1. 在 Debug 构建中复现,并在最窄的有用断点处停止。
  2. 在不执行代码的情况下检查局部变量,然后捕获当前堆栈。
  3. 移动到相关帧或线程,验证失败状态。
  4. 仅在错误转换仍不明确时添加条件或监视点。
(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 获取完整的检查、断点/日志点、表达式、监视点、线程导航和符号断点命令表。

内存调试

内存图调试器工作流

  1. 在 Debug 配置下运行应用。
  2. 复现疑似泄漏(导航到某个屏幕,然后返回)。
  3. 点击 Xcode 调试栏中的内存图按钮。
  4. 查找紫色警告图标——这些表示泄漏的对象。
  5. 选择一个泄漏对象以查看其引用图和回溯。

在运行前启用 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_signpostOSSignposter:标记 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

  1. Product > Profile(Cmd+I)启动 Instruments。
  2. 选择 Time Profiler 模板。
  3. 在复现慢速交互时录制。
  4. 聚焦主线程——按“权重”排序以查找热点路径。
  5. 勾选“隐藏系统库”以仅查看你的代码。
  6. 双击重帧跳转到源代码。

常见卡顿原因

原因 症状 修复
主线程同步 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_PATHS
  • OTHER_LDFLAGS
  • SWIFT_INCLUDE_PATHS
  • BUILD_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 进行自定义性能时间间隔
  • [ ] 对循环/集合调试使用条件断点

参考资料