debugging-instruments

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Debugging and Instruments

调试与Instruments工具

Contents

目录

LLDB Debugging

LLDB调试

Essential Commands

核心命令

text
(lldb) po myObject              # Print object description (calls debugDescription)
(lldb) p myInt                  # Print with type info (uses LLDB formatter)
(lldb) v myLocal                # Frame variable — fast, no code execution
(lldb) bt                       # Backtrace current thread
(lldb) bt all                   # Backtrace all threads
(lldb) frame select 3           # Jump to frame #3 in the backtrace
(lldb) thread list              # List all threads and their states
(lldb) thread select 4          # Switch to thread #4
Use
v
over
po
when you only need a local variable value — it does not execute code and cannot trigger side effects.
text
(lldb) po myObject              # 打印对象描述(调用debugDescription方法)
(lldb) p myInt                  # 带类型信息打印(使用LLDB格式化器)
(lldb) v myLocal                # 帧变量——速度快,不执行代码
(lldb) bt                       # 当前线程的回溯信息
(lldb) bt all                   # 所有线程的回溯信息
(lldb) frame select 3           # 跳转到回溯信息中的第3帧
(lldb) thread list              # 列出所有线程及其状态
(lldb) thread select 4          # 切换到第4个线程
当你只需要获取局部变量的值时,优先使用
v
而非
po
——它不会执行代码,也不会触发副作用。

Breakpoint Management

断点管理

text
(lldb) br set -f ViewModel.swift -l 42          # Break at file:line
(lldb) br set -n viewDidLoad                     # Break on function name
(lldb) br set -S setValue:forKey:                 # Break on ObjC selector
(lldb) br modify 1 -c "count > 10"              # Add condition to breakpoint 1
(lldb) br modify 1 --auto-continue true          # Log and continue (logpoint)
(lldb) br command add 1                          # Attach commands to breakpoint
> po self.title
> continue
> DONE
(lldb) br disable 1                              # Disable without deleting
(lldb) br delete 1                               # Remove breakpoint
text
(lldb) br set -f ViewModel.swift -l 42          # 在指定文件的指定行设置断点
(lldb) br set -n viewDidLoad                     # 根据函数名设置断点
(lldb) br set -S setValue:forKey:                 # 根据OC选择器设置断点
(lldb) br modify 1 -c "count > 10"              # 为编号1的断点添加条件
(lldb) br modify 1 --auto-continue true          # 日志断点:记录信息后继续执行
(lldb) br command add 1                          # 为编号1的断点附加执行命令
> po self.title
> continue
> DONE
(lldb) br disable 1                              # 禁用断点(不删除)
(lldb) br delete 1                               # 删除断点

Expression Evaluation

表达式求值

text
(lldb) expr myArray.count                        # Evaluate Swift expression
(lldb) e -l swift -- import UIKit                # Import framework in LLDB
(lldb) e -l swift -- self.view.backgroundColor = .red  # Modify state at runtime
(lldb) e -l objc -- (void)[CATransaction flush]  # Force UI update after changes
After modifying a view property in the debugger, call
CATransaction.flush()
to see the change immediately without resuming execution.
text
(lldb) expr myArray.count                        # 求值Swift表达式
(lldb) e -l swift -- import UIKit                # 在LLDB中导入框架
(lldb) e -l swift -- self.view.backgroundColor = .red  # 在运行时修改状态
(lldb) e -l objc -- (void)[CATransaction flush]  # 修改后强制更新UI
在调试器中修改视图属性后,调用
CATransaction.flush()
可以立即看到变化,无需恢复执行。

Watchpoints

观察点

text
(lldb) w set v self.score                        # Break when score changes
(lldb) w set v self.score -w read               # Break when score is read
(lldb) w modify 1 -c "self.score > 100"         # Conditional watchpoint
(lldb) w list                                    # Show active watchpoints
(lldb) w delete 1                                # Remove watchpoint
Watchpoints are hardware-backed (limited to ~4 on ARM). Use them to find unexpected mutations — the debugger stops at the exact line that changes the value.
text
(lldb) w set v self.score                        # 当score值变化时触发断点
(lldb) w set v self.score -w read               # 当score被读取时触发断点
(lldb) w modify 1 -c "self.score > 100"         # 为编号1的观察点添加条件
(lldb) w list                                    # 显示所有活跃的观察点
(lldb) w delete 1                                # 删除观察点
观察点由硬件支持(ARM架构下最多支持约4个)。使用它们可以定位意外的变量修改——调试器会在修改值的代码行处暂停。

Symbolic Breakpoints

符号断点

Set breakpoints on methods without knowing the file. Useful for framework or system code:
text
(lldb) br set -n "UIViewController.viewDidLoad"
(lldb) br set -r ".*networkError.*"              # Regex on symbol name
(lldb) br set -n malloc_error_break              # Catch malloc corruption
(lldb) br set -n UIViewAlertForUnsatisfiableConstraints  # Auto Layout issues
In Xcode, use the Breakpoint Navigator (+) to add symbolic breakpoints for common diagnostics like
-[UIApplication main]
or
swift_willThrow
.
无需知道文件路径即可为方法设置断点,适用于框架或系统代码:
text
(lldb) br set -n "UIViewController.viewDidLoad"
(lldb) br set -r ".*networkError.*"              # 根据符号名的正则表达式设置断点
(lldb) br set -n malloc_error_break              # 捕获内存分配损坏问题
(lldb) br set -n UIViewAlertForUnsatisfiableConstraints  # 自动布局问题
在Xcode中,使用断点导航器(+按钮)可以添加常见诊断场景的符号断点,比如
-[UIApplication main]
swift_willThrow

Memory Debugging

内存调试

Memory Graph Debugger Workflow

Memory Graph Debugger使用流程

  1. Run the app in Debug configuration.
  2. Reproduce the suspected leak (navigate to a screen, then back).
  3. Tap the Memory Graph button in Xcode's debug bar.
  4. Look for purple warning icons — these indicate leaked objects.
  5. Select a leaked object to see its reference graph and backtrace.
Enable Malloc Stack Logging (Scheme > Diagnostics) before running so the Memory Graph shows allocation backtraces.
  1. 以Debug配置运行应用。
  2. 复现疑似内存泄漏的场景(比如进入某个页面后返回)。
  3. 点击Xcode调试栏中的内存图按钮。
  4. 寻找紫色警告图标——这些表示存在泄漏的对象。
  5. 选择泄漏对象查看其引用图和回溯信息。
运行前启用Malloc Stack Logging(Scheme > Diagnostics),这样内存图会显示分配回溯信息。

Common Retain Cycle Patterns

常见引用循环模式

Closure capturing self strongly:
swift
// 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()
    }
}
Strong delegate reference:
swift
// 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?
}
Timer retaining target:
swift
// 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()
}
闭包强引用self:
swift
// 内存泄漏——闭包强引用self
class ProfileViewModel {
    var onUpdate: (() -> Void)?

    func startObserving() {
        onUpdate = {
            self.refresh()  // 强引用self
        }
    }
}

// 修复方案——使用[weak self]
func startObserving() {
    onUpdate = { [weak self] in
        self?.refresh()
    }
}
代理强引用:
swift
// 内存泄漏——强引用代理形成循环
protocol DataDelegate: AnyObject {
    func didUpdate()
}

class DataManager {
    var delegate: DataDelegate?  // 应该使用weak
}

// 修复方案——使用weak代理
class DataManager {
    weak var delegate: DataDelegate?
}
Timer强引用目标对象:
swift
// 内存泄漏——Timer.scheduledTimer会强引用目标对象
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 and Leaks

Instruments:内存分配与泄漏检测

  • Allocations template: Track memory growth over time. Use the "Mark Generation" feature to isolate allocations created between user actions (e.g., open/close a screen).
  • Leaks template: Automatically detects reference cycles at runtime. Run alongside Allocations for a complete picture.
  • Filter by your app's module name to exclude system allocations.
  • Allocations模板:跟踪内存随时间的增长情况。使用“Mark Generation”功能可以隔离用户操作之间(比如打开/关闭页面)创建的内存分配。
  • Leaks模板:在运行时自动检测引用循环。配合Allocations模板使用可以获得完整的内存情况。
  • 按应用的模块名称过滤,排除系统内存分配。

Malloc Stack Logging

Malloc Stack Logging

Enable in Scheme > Run > Diagnostics > Malloc Stack Logging (All Allocations). This records the call stack for every allocation, letting the Memory Graph Debugger and
leaks
CLI show where objects were created.
bash
undefined
在Scheme > Run > Diagnostics > Malloc Stack Logging (All Allocations)中启用。它会记录每次内存分配的调用栈,让Memory Graph Debugger和
leaks
命令行工具可以显示对象的创建位置。
bash
undefined

CLI leak detection

命令行泄漏检测

leaks --atExit -- ./MyApp.app/MyApp
leaks --atExit -- ./MyApp.app/MyApp

Symbolicate with dSYMs for readable stacks

使用dSYM文件符号化以获得可读的调用栈

undefined
undefined

Hang Diagnostics

卡顿诊断

Identifying Main Thread Hangs

识别主线程卡顿

A hang occurs when the main thread is blocked for > 250ms (noticeable) or
1s (severe). Common detection tools:
  • Thread Checker (Xcode Diagnostics): warns about non-main-thread UI calls
  • os_signpost and
    OSSignposter
    : mark intervals for Instruments
  • MetricKit hang diagnostics: production hang detection (see
    metrickit-diagnostics
    skill for
    MXHangDiagnostic
    )
swift
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)
}
当主线程被阻塞超过250ms(可感知卡顿)或1s(严重卡顿)时,就会出现卡顿。常用检测工具:
  • Thread Checker(Xcode Diagnostics):警告非主线程的UI操作
  • os_signpost
    OSSignposter
    :为Instruments标记时间区间
  • MetricKit卡顿诊断:生产环境卡顿检测(查看
    metrickit-diagnostics
    技能中的
    MXHangDiagnostic
swift
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)
}

Using the Time Profiler

使用Time Profiler

  1. Product > Profile (Cmd+I) to launch Instruments.
  2. Select the Time Profiler template.
  3. Record while reproducing the slow interaction.
  4. Focus on the main thread — sort by "Weight" to find hot paths.
  5. Check "Hide System Libraries" to see only your code.
  6. Double-click a heavy frame to jump to source.
  1. 选择Product > Profile(Cmd+I)启动Instruments。
  2. 选择Time Profiler模板。
  3. 复现缓慢交互的同时进行录制。
  4. 聚焦主线程——按“Weight”排序找到热点路径。
  5. 勾选“Hide System Libraries”只查看自己的代码。
  6. 双击权重高的帧跳转到源代码。

Common Hang Causes

常见卡顿原因

CauseSymptomFix
Synchronous I/O on main threadNetwork/file reads block UIMove to
Task { }
or background actor
Lock contentionMain thread waiting on a lock held by background workUse actors or reduce lock scope
Layout thrashingRepeated
layoutSubviews
calls
Batch layout changes, avoid forced layout
JSON parsing large payloadsUI freezes during data loadParse on a background thread
Synchronous image decodingScroll jank on image-heavy listsUse
AsyncImage
or decode off main thread
原因症状修复方案
主线程同步I/O网络/文件读取阻塞UI移到
Task { }
或后台actor中执行
锁竞争主线程等待后台线程持有的锁使用actor或缩小锁的作用域
布局抖动重复调用
layoutSubviews
批量处理布局变化,避免强制布局
大JSON解析数据加载时UI冻结在后台线程解析
同步图片解码图片密集列表滚动卡顿使用
AsyncImage
或在后台线程解码

Build Failure Triage

构建故障排查

Reading Compiler Diagnostics

解读编译器诊断信息

  • Start from the first error — subsequent errors are often cascading.
  • Search for the error code (e.g.,
    error: cannot convert
    ) in the build log.
  • Use Report Navigator (Cmd+9) for the full build log with timestamps.
  • 第一个错误开始排查——后续错误通常是连锁反应。
  • 在构建日志中搜索错误代码(比如
    error: cannot convert
    )。
  • 使用报告导航器(Cmd+9)查看带时间戳的完整构建日志。

SPM Dependency Resolution

SPM依赖解析

text
undefined
text
undefined

Common: version conflict

常见问题:版本冲突

error: Dependencies could not be resolved because root depends on 'Package' 1.0.0..<2.0.0
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

修复方案:检查Package.resolved并更新版本范围

Reset package caches if needed:

必要时重置包缓存:

rm -rf ~/Library/Caches/org.swift.swiftpm rm -rf .build swift package resolve
undefined
rm -rf ~/Library/Caches/org.swift.swiftpm rm -rf .build swift package resolve
undefined

Module Not Found / Linker Errors

模块未找到/链接器错误

ErrorCheck
No such module 'Foo'
Target membership, import paths, framework search paths
Undefined symbol
Linking phase missing framework, wrong architecture
duplicate symbol
Two targets define same symbol; check for ObjC naming collisions
Build settings to inspect first:
  • FRAMEWORK_SEARCH_PATHS
  • OTHER_LDFLAGS
  • SWIFT_INCLUDE_PATHS
  • BUILD_LIBRARY_FOR_DISTRIBUTION
    (for XCFrameworks)
错误检查项
No such module 'Foo'
目标成员资格、导入路径、框架搜索路径
Undefined symbol
链接阶段缺少框架、架构不匹配
duplicate symbol
两个目标定义了相同的符号;检查OC命名冲突
首先检查以下构建设置:
  • FRAMEWORK_SEARCH_PATHS
  • OTHER_LDFLAGS
  • SWIFT_INCLUDE_PATHS
  • BUILD_LIBRARY_FOR_DISTRIBUTION
    (适用于XCFrameworks)

Instruments Overview

Instruments工具概述

Template Selection Guide

模板选择指南

TemplateUse When
Time ProfilerCPU is high, UI feels slow, need to find hot code paths
AllocationsMemory grows over time, need to track object lifetimes
LeaksSuspect retain cycles or abandoned objects
NetworkInspecting HTTP request/response timing and payloads
SwiftUIProfiling view body evaluations and update frequency
Core AnimationFrame drops, off-screen rendering, blending issues
Energy LogBattery drain, background energy impact
File ActivityExcessive disk I/O, slow file operations
System TraceThread scheduling, syscalls, virtual memory faults
模板适用场景
Time ProfilerCPU占用高、UI响应慢,需要找到热点代码路径
Allocations内存持续增长,需要跟踪对象生命周期
Leaks疑似引用循环或废弃对象
Network检查HTTP请求/响应的时间和负载
SwiftUI分析视图body的求值频率和更新情况
Core Animation掉帧、离屏渲染、混合问题
Energy Log电池耗电快、后台能耗高
File Activity磁盘I/O过多、文件操作缓慢
System Trace线程调度、系统调用、虚拟内存错误

xctrace CLI for CI Profiling

用于CI环境分析的xctrace命令行工具

bash
undefined
bash
undefined

Record a trace from the command line

从命令行录制跟踪数据

xcrun xctrace record --device "My iPhone"
--template "Time Profiler"
--output profile.trace
--launch MyApp.app
xcrun xctrace record --device "My iPhone"
--template "Time Profiler"
--output profile.trace
--launch MyApp.app

Export trace data as XML for automated analysis

将跟踪数据导出为XML以进行自动化分析

xcrun xctrace export --input profile.trace --xpath '/trace-toc/run/data/table'
xcrun xctrace export --input profile.trace --xpath '/trace-toc/run/data/table'

List available templates

列出可用模板

xcrun xctrace list templates
xcrun xctrace list templates

List connected devices

列出已连接设备

xcrun xctrace list devices

Use `xctrace` in CI pipelines to catch performance regressions
automatically. Compare exported metrics between builds.
xcrun xctrace list devices

在CI流水线中使用`xctrace`可以自动捕获性能回归问题。对比不同构建版本的导出指标。

Common Mistakes

常见误区

DON'T: Use print() for debugging instead of os.Logger

不要:使用print()调试而非os.Logger

print()
output is not filterable, has no log levels, and is not automatically stripped from release builds. It pollutes the console and makes it impossible to isolate relevant output.
swift
// WRONG — unstructured, not filterable, stays in release builds
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)")
Logger
messages appear in Console.app with filtering by subsystem and category, and
.debug
messages are automatically excluded from release builds.
print()
的输出无法过滤,没有日志级别,且不会自动从Release构建中移除。它会污染控制台,无法隔离相关输出。
swift
// 错误用法——非结构化、不可过滤,会保留在Release构建中
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)")
Logger
的消息会出现在Console.app中,可以按subsystem和category过滤,且
.debug
级别的消息会自动从Release构建中排除。

DON'T: Forget to enable Malloc Stack Logging before memory debugging

不要:内存调试前忘记启用Malloc Stack Logging

Without Malloc Stack Logging, the Memory Graph Debugger shows leaked objects but cannot display allocation backtraces, making it difficult to find the code that created them.
swift
// 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
如果没有启用Malloc Stack Logging,Memory Graph Debugger会显示泄漏的对象,但无法显示分配调用栈,难以找到创建对象的代码。
swift
// 错误用法——未启用Malloc Stack Logging就打开Memory Graph
// 结果:可以看到泄漏对象,但没有分配调用栈

// 正确用法——运行前启用:
// Scheme > Run > Diagnostics > 勾选"Malloc Stack Logging: All Allocations"
// 然后运行应用,复现泄漏场景,再打开Memory Graph

DON'T: Debug optimized code expecting full variable visibility

不要:调试优化后的代码并期望完整的变量可见性

In Release (optimized) builds, the compiler may inline functions, eliminate variables, and reorder code. LLDB cannot display optimized-away values.
swift
// 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)
在Release(优化)构建中,编译器可能会内联函数、消除变量、重排代码。LLDB无法显示被优化掉的变量值。
swift
// 错误用法——用Debug构建做性能分析,用Release构建做调试
// Debug构建:额外的运行时检查会影响性能测量结果
// Release构建:变量在调试器中显示为"<optimized out>"

// 正确做法:
// 调试:使用Debug配置(完整符号,无优化)
// 性能分析:使用Release配置(真实性能)

DON'T: Stop on every loop iteration without conditional breakpoints

不要:不使用条件断点就暂停循环的每次迭代

Breaking on every iteration wastes time and makes it hard to find the specific case you care about.
swift
// 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
在循环内的代码行设置断点会导致每次迭代都暂停,浪费时间且难以找到关心的特定场景。
swift
// 错误用法——在循环内的代码行设置断点,会暂停10000次
for item in items {
    process(item)  // 此处的断点会在每个item时暂停
}

// 正确用法——使用条件断点:
// (lldb) br set -f MyFile.swift -l 42 -c "item.id == targetID"
// 或在Xcode中:右键断点 > 编辑 > 添加条件

DON'T: Ignore Thread Sanitizer warnings

不要:忽略Thread Sanitizer警告

Thread Sanitizer (TSan) warnings indicate data races that may only crash intermittently. They are real bugs, not false positives.
swift
// 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 }
}
Enable TSan: Scheme > Run > Diagnostics > Thread Sanitizer. Note: TSan cannot run simultaneously with Address Sanitizer.
Thread Sanitizer(TSan)警告表示存在数据竞争,可能会导致间歇性崩溃。它们是真实的bug,不是误报。
swift
// 错误用法——忽略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。注意:TSan无法与Address Sanitizer同时运行。

Review Checklist

检查清单

  • Using
    os.Logger
    instead of
    print()
    for diagnostic output
  • Malloc Stack Logging enabled before memory debugging sessions
  • Memory Graph Debugger checked after dismiss/dealloc flows
  • Delegates declared as
    weak var
    to prevent retain cycles
  • Closures stored as properties use
    [weak self]
    capture lists
  • Timers use closure-based API with
    [weak self]
  • Thread Sanitizer enabled in test schemes
  • No synchronous I/O or heavy computation on the main thread
  • Time Profiler run on Release build for performance baselines
  • Build failures triaged from the first error in the build log
  • OSSignposter
    used for custom performance intervals
  • Conditional breakpoints used for loop/collection debugging
  • 使用
    os.Logger
    而非
    print()
    输出诊断信息
  • 内存调试会话前已启用Malloc Stack Logging
  • 关闭/销毁流程后已检查Memory Graph Debugger
  • 代理声明为
    weak var
    以避免引用循环
  • 作为属性存储的闭包使用
    [weak self]
    捕获列表
  • Timer使用基于闭包的API并配合
    [weak self]
  • 测试Scheme中已启用Thread Sanitizer
  • 主线程无同步I/O或重计算操作
  • 已使用Release配置运行Time Profiler获取性能基准
  • 构建故障从第一个错误开始排查
  • 使用
    OSSignposter
    标记自定义性能区间
  • 循环/集合调试使用条件断点

References

参考资料