MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift调试技巧与LLDB命令大全

2023-06-242.3k 阅读

Swift 调试基础

在 Swift 开发过程中,调试是不可或缺的环节,它能帮助开发者快速定位并解决代码中的问题。Swift 调试主要借助 Xcode 集成开发环境以及 LLDB(Low Level Debugger)调试工具。

断点设置

断点是调试过程中最基本的工具,它允许程序在运行到特定代码行时暂停执行,以便开发者检查变量的值、调用栈等信息。

  1. 普通断点:在 Xcode 的代码编辑器中,点击代码行号左侧的空白区域即可添加一个普通断点。当程序运行到该断点所在行时,会暂停执行。
let num1 = 10
let num2 = 20
let sum = num1 + num2
print("The sum is: \(sum)")

在上述代码中,如果在 let sum = num1 + num2 这一行添加断点,程序运行到此处就会暂停。 2. 条件断点:有时候我们希望断点仅在满足特定条件时触发,这就需要设置条件断点。右键点击普通断点,选择 “Edit Breakpoint”,在弹出的窗口中可以设置条件。例如,假设我们有一个循环:

for i in 1...10 {
    let result = i * 2
    if i == 5 {
        print("The result at i = 5 is: \(result)")
    }
}

如果我们只想在 i == 5 时暂停程序,可以在 let result = i * 2 这一行设置条件断点,条件为 i == 5。这样,程序在循环过程中只有当 i 等于 5 时才会在该断点处暂停。 3. 符号断点:符号断点用于在特定函数或方法被调用时暂停程序。在 Xcode 的 “Breakpoint Navigator” 中点击 “+” 号,选择 “Symbolic Breakpoint”。在 “Symbol” 字段中输入要中断的函数名,例如 print。这样,当代码中调用 print 函数时,程序就会暂停。

调试导航栏

Xcode 的调试导航栏提供了一系列在调试过程中常用的功能:

  1. 继续执行:点击“继续执行”按钮(看起来像一个三角形),程序会从当前暂停点继续执行,直到遇到下一个断点或程序结束。
  2. 单步执行:单步执行按钮(一个向右的箭头)会使程序执行下一行代码,如果下一行代码是一个函数调用,它会进入函数内部。
func multiply(a: Int, b: Int) -> Int {
    return a * b
}
let num3 = 3
let num4 = 4
let product = multiply(a: num3, b: num4)
print("The product is: \(product)")

如果在 let product = multiply(a: num3, b: num4) 这一行暂停,点击单步执行,会进入 multiply 函数内部。 3. 单步跳过:单步跳过按钮(一个向右的箭头,中间有一个小横杠)会执行下一行代码,但如果下一行是函数调用,它不会进入函数内部,而是直接执行完函数并停留在函数调用后的下一行。在上述代码中,如果点击单步跳过,程序会直接执行完 multiply 函数并停留在 print("The product is: \(product)") 这一行。 4. 单步跳出:单步跳出按钮(一个向左的箭头)用于从当前函数内部跳出,返回到调用该函数的地方。如果在 multiply 函数内部暂停,点击单步跳出,会回到 let product = multiply(a: num3, b: num4) 之后的代码行。

LLDB 基础

LLDB 是 Xcode 内置的调试器,它提供了强大的命令行调试功能。虽然 Xcode 的图形界面调试功能已经很强大,但 LLDB 命令在某些复杂调试场景下非常有用。

LLDB 启动

当程序在断点处暂停时,Xcode 底部的调试区域会显示 LLDB 命令行界面。你可以在这个界面中输入 LLDB 命令。另外,也可以通过在终端中输入 lldb 命令来启动 LLDB,然后使用 target create 命令加载要调试的可执行文件。例如,如果你的 Swift 应用程序编译后的可执行文件名为 MyApp,在终端中进入该文件所在目录后执行 lldb MyApp 即可启动 LLDB 调试该应用。

LLDB 常用命令

  1. breakpoint 命令:用于管理断点。
    • breakpoint set -l <line number>:在指定行号处设置断点。例如,假设你的代码文件名为 main.swift,要在第 10 行设置断点,可以在 LLDB 中执行 breakpoint set -l 10 -f main.swift
    • breakpoint list:列出当前所有断点的信息,包括断点编号、所在文件、行号等。
    • breakpoint delete <breakpoint number>:删除指定编号的断点。可以通过 breakpoint list 查看断点编号,然后执行 breakpoint delete 1 删除编号为 1 的断点。
  2. frame 命令:用于检查和操作当前栈帧。
    • frame variable:打印当前栈帧中的所有变量及其值。例如,在前面计算 sum 的代码中,当程序在 print("The sum is: \(sum)") 这一行暂停时,执行 frame variable 会显示 num1num2sum 的值。
    • frame select <frame number>:切换到指定编号的栈帧。在多层函数调用的情况下,程序暂停时可能有多个栈帧,通过这个命令可以查看不同栈帧中的变量。
  3. expression 命令:用于在调试过程中计算表达式的值。
    • expression <expression>:计算并输出指定表达式的值。例如,在计算 product 的代码中,当程序暂停时,可以执行 expression num3 * num4,LLDB 会输出表达式的计算结果 12。
    • expression -l swift -O -- <swift code>:执行一段 Swift 代码。比如 expression -l swift -O -- let newProduct = num3 * num4 * 2; print(newProduct),会在调试环境中执行这段 Swift 代码并输出结果。
  4. thread 命令:用于管理线程。
    • thread backtrace:打印当前线程的调用栈信息,显示函数调用的顺序。
    • thread step-over:单步跳过,功能与 Xcode 调试导航栏中的单步跳过按钮相同。
    • thread step-in:单步执行,进入函数内部,与 Xcode 调试导航栏中的单步执行按钮功能一致。
    • thread step-out:单步跳出,从当前函数内部跳出,对应 Xcode 调试导航栏中的单步跳出按钮。

高级 Swift 调试技巧

除了基本的断点设置和 LLDB 命令使用,还有一些高级调试技巧可以帮助开发者更高效地解决复杂问题。

调试内存问题

  1. 内存泄漏检测:Swift 中虽然有自动内存管理(ARC - Automatic Reference Counting),但在某些情况下仍可能出现内存泄漏。Xcode 提供了 Instruments 工具来检测内存泄漏。打开 Instruments,选择 “Leaks” 模板,然后运行你的应用程序。Instruments 会实时监测内存使用情况,当发现内存泄漏时会标记出来,并提供相关的堆栈信息,帮助你定位泄漏发生的位置。
  2. 悬空指针检测:悬空指针是指指向已释放内存的指针。在 Swift 中,由于 ARC 的存在,悬空指针问题相对较少,但在与 C 语言等交互时仍可能出现。Xcode 的 Address Sanitizer 工具可以检测悬空指针。在 Xcode 项目的 “Build Settings” 中,搜索 “Address Sanitizer”,将 “Enable Address Sanitizer” 设置为 “Yes”。运行应用程序时,如果出现悬空指针访问,Xcode 会捕获并给出详细的错误信息,包括出错的代码行。

调试多线程问题

  1. 线程断点:在多线程编程中,设置线程断点可以帮助你控制特定线程的执行。在 Xcode 的 “Breakpoint Navigator” 中添加一个普通断点,然后右键点击该断点,选择 “Edit Breakpoint”。在弹出的窗口中,勾选 “Thread” 选项,并选择要中断的线程。这样,当指定线程执行到该断点时,程序会暂停。
  2. 数据竞争检测:数据竞争是多线程编程中常见的问题,当多个线程同时访问和修改共享数据时可能会发生。Xcode 的 Thread Sanitizer 工具可以检测数据竞争。在 Xcode 项目的 “Build Settings” 中,将 “Enable Thread Sanitizer” 设置为 “Yes”。运行应用程序时,如果检测到数据竞争,Xcode 会给出详细的错误信息,包括竞争发生的代码位置和涉及的线程。

调试异步代码

  1. 异步断点:Swift 中的异步代码(如使用 DispatchQueueasync/await)调试起来可能比较棘手。Xcode 11 及以上版本支持异步断点。在 “Breakpoint Navigator” 中添加一个普通断点,右键点击并选择 “Edit Breakpoint”。在弹出窗口中,勾选 “Suspend on” 下的 “All Threads”,并选择 “Async”。这样,当异步任务执行到该断点时,程序会暂停,方便你检查异步代码中的变量和执行流程。
  2. 使用 Task 调试 async/await:在 Swift 5.5 引入的 async/await 编程模型中,可以使用 Task 的一些特性来辅助调试。例如,可以在 Task 中添加日志输出,通过观察日志来了解异步任务的执行情况。
func asyncFunction() async {
    print("Async function started")
    await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    print("Async function finished")
}

Task {
    await asyncFunction()
}

在上述代码中,通过打印日志可以清楚地看到异步函数的开始和结束时间,帮助调试异步流程。

深入 LLDB 命令

除了前面介绍的基础 LLDB 命令,还有一些更深入的命令可以满足复杂调试需求。

观察点

观察点用于在变量的值发生变化时暂停程序。

  1. 设置观察点:使用 watchpoint set variable <variable name> 命令设置观察点。例如,假设有一个变量 count,在 LLDB 中执行 watchpoint set variable count,当 count 的值发生改变时,程序会暂停。
  2. 观察点列表与删除:使用 watchpoint list 可以查看当前设置的所有观察点信息,包括观察点编号。使用 watchpoint delete <watchpoint number> 可以删除指定编号的观察点。

断点条件表达式

在设置断点时,可以使用更复杂的条件表达式。例如,假设我们有一个数组 let numbers = [1, 2, 3, 4, 5],并且有一个循环遍历数组:

for (index, number) in numbers.enumerated() {
    if number % 2 == 0 {
        print("The even number at index \(index) is \(number)")
    }
}

如果我们想在数组中的偶数且索引大于 2 时暂停程序,可以在 if number % 2 == 0 这一行设置断点,然后在 LLDB 中使用 breakpoint modify -c "number % 2 == 0 && index > 2" 来修改断点条件。这样,只有当满足这个复杂条件时,断点才会触发。

别名与宏

  1. 别名:LLDB 允许为常用命令设置别名,提高调试效率。使用 command alias <alias name> <lldb command> 命令设置别名。例如,经常使用 frame variable 查看变量,可以设置别名 command alias fv frame variable,之后在 LLDB 中输入 fv 就相当于执行 frame variable
  2. :宏是一段自定义的 LLDB 命令序列。可以使用 command script add -f <function name> <macro name> 来定义宏。首先需要在 Python 脚本中定义一个函数,例如:
def my_macro(debugger, command, result, internal_dict):
    import lldb
    debugger.HandleCommand('frame variable')
    debugger.HandleCommand('thread backtrace')

然后在 LLDB 中执行 command script import /path/to/your/script.py 导入脚本,再执行 command script add -f my_macro my_macro 定义宏。之后输入 my_macro 就会依次执行 frame variablethread backtrace 命令。

调试 Swift 类与对象

  1. 查看类信息:使用 image lookup -t <class name> 命令可以查看类的详细信息,包括类的定义、属性、方法等。例如,假设有一个类 Person
class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    func introduce() {
        print("Hi, I'm \(name) and I'm \(age) years old.")
    }
}
let person = Person(name: "John", age: 30)

在 LLDB 中执行 image lookup -t Person 可以查看 Person 类的结构。 2. 访问对象属性:当程序在包含对象的代码行暂停时,可以使用 expression -l swift -O -- <object name>.<property name> 来访问对象的属性。例如,在上述代码中,当程序暂停在 let person = Person(name: "John", age: 30) 之后,可以执行 expression -l swift -O -- person.name 来获取 person 对象的 name 属性值。

调试 Swift 与其他语言的混合代码

在实际开发中,Swift 项目可能会与其他语言(如 C、C++、Objective - C)混合使用,调试这种混合代码需要一些特殊技巧。

Swift 与 Objective - C 混合调试

  1. 桥接头文件:在 Swift 与 Objective - C 混合编程时,需要创建桥接头文件来使两者相互调用。在调试时,确保桥接头文件配置正确。如果在调用 Objective - C 代码时出现问题,可以在桥接头文件中添加日志输出,或者在 Objective - C 方法中设置断点,通过 Xcode 的调试工具来定位问题。
  2. 数据类型转换:Swift 和 Objective - C 数据类型有一些差异,在调试时需要注意数据类型转换。例如,Objective - C 的 NSString 在 Swift 中对应 String。如果在两者之间传递数据出现错误,可以在转换的代码处设置断点,检查数据的实际类型和值。

Swift 与 C/C++ 混合调试

  1. 外部函数接口(FFI):Swift 通过 @_cdecl 等属性与 C 函数进行交互。在调试时,确保 C 函数的声明和实现正确。可以在 C 函数中添加 printf 等输出语句,同时在 Swift 调用 C 函数的地方设置断点,结合 LLDB 命令检查参数传递是否正确。
  2. 内存管理:C 和 C++ 没有 ARC 这样的自动内存管理机制,在与 Swift 混合使用时,需要特别注意内存管理。例如,从 C 函数返回的内存需要在 Swift 中正确释放。可以使用 Address Sanitizer 等工具来检测内存错误,同时在涉及内存分配和释放的代码处设置断点,仔细检查内存操作。

在调试 Swift 与其他语言的混合代码时,需要充分利用 Xcode 的调试工具和 LLDB 命令,结合不同语言的特点,逐步排查问题,确保代码的正确性和稳定性。通过不断实践和积累经验,开发者能够更熟练地处理各种复杂的混合编程调试场景。