大多数浏览器和
Developer App 均支持流媒体播放。
-
运行、暂停、检查:探索如何使用 LLDB 进行有效调试
了解如何使用 LLDB 来探索和调试代码库。我们将介绍如何充分利用崩溃日志和回溯栈跟踪,以及如何通过操作和复杂停止条件来优化断点流程。我们还将探索 Swift 6 中的“p”命令和最新功能可以如何帮你优化调试体验。
章节
- 0:00 - Introduction
- 0:42 - Agenda
- 1:15 - Debugging as a search problem
- 4:07 - Crashlogs & starting the program
- 7:27 - Breakpoints
- 12:10 - Breakpoint actions
- 15:27 - Help command
- 16:05 - High-firing breakpoints
- 19:24 - The p command
- 25:39 - @DebugDescription macro
- 27:50 - Wrap-up
资源
相关视频
WWDC23
WWDC21
-
下载
大家好 我叫 Felipe 是 Apple 调试技术团队 的一名工程师 在本讲座中 我们 将讨论各种调试技巧 帮助你轻松探索代码 并更快地找出错误 LLDB 是 Xcode 附带的底层调试器 能够随时暂停你的程序 检查变量状态 计算表达式等等 在本讲座中 我们将介绍 LLDB 提供的主要工具 同时展示 你可能不太熟悉的 新功能和高级技巧 我们将首先定义一个调试模型 来引导我们使用调试器 然后介绍另一种使用崩溃日志 进行调试的方法 我们将探索使用断点 暂停程序执行的不同方法 以及某些编程模式 如何与调试器交互 然后 我们将了解用于检查 程序状态的主要工具即“p”命令 最后 我们将介绍 Swift 6 的一项新功能 它可让我们自定数据类型 在调试器中的显示方式 针对程序问题进行调试时 我们通常会知道 程序出现错误的时间点 这个错误可能是崩溃、 显示了错误的值 甚至是程序挂起 从程序开始执行时 到观察到错误行为的时间点 这之间的某个时间 执行了错误的代码 我们的目标就是找到那段代码 我们通常可以通过 检查不同时间点的 程序状态来找到错误 每个检查都会让我们 离找到问题代码更进一步 我们可能对一些用于检查 程序状态的不同技巧并不陌生 例如 一些代码库会使用日志语句 在这种情况下 读取日志文件的 某个条目 类似于检查 程序中的某个时间点 如果日志足够详细 可能无需其他操作 就能精准找到代码中的错误 这种方法需要程序员有先见之明 能够确定哪些是需要记录的有用信息 但它也是一种在用户 和开发者之间传输 App 诊断信息的有效技巧 另一种经常使用的技巧是打印调试 这可能是我们所有人 学习的第一种调试方法 利用打印调试 我们可以 在程序中插入 print 语句 重新编译代码 运行程序并重现错误 最后检查打印出来的信息 如果需要打印新内容 就重复整个过程 最后 我们可以检查足够多的 程序状态来修复错误 但最好不要忘记 在完成调试后移除 print 语句 因为我们都有过将 print 语句 带入生产环境中的有趣经历 整个过程可能非常耗时 并且容易出错 在本讲座中 我们将展示 如何使用调试器更快速地 在搜索空间中找到所需内容 为此 接下来我们将讨论 LLDB 提供的主要工具: 回溯栈跟踪 变量检查 断点 以及表达式求值 我们还将展示 LLDB 如何帮助你在不运行 程序的情况下调查问题 使用调试器时 我们一直在重复三个不同的操作 运行程序 在需要关注的地方暂停 然后检查程序状态 对程序进行检查后 我们可以继续检查 程序执行的下一个时间点 或者 如果需要检查前一个 时间点 可以重新启动程序 高效地反复完成 运行、暂停、检查这一循环 是实现有效调试会话的关键 我们来实际操作一下 大多数调试会话首先都会编译代码 并在调试器中运行应用程序 通常 我们只需点按 Xcode 中的开始按钮 或使用目标可执行文件及其参数 通过命令行启动 LLDB 不过 调试问题的 第一步是重现错误 LLDB 也能提供这方面的帮助 所使用的技巧 甚至无需启动应用程序 在 Apple 平台上 每次发生程序崩溃时 系统都会收集 程序崩溃时的状态信息 并创建崩溃日志 LLDB 能够读取崩溃日志 并以类似调试会话的形式显示出来 让开发者能够对崩溃情况 进行初步调查 有时 我们可能只需要查看 这份日志 就能查明错误来源 我的一位同事给我发送了 一份崩溃日志 这位同事正在测试多平台 视频播放 App “Destination Video” 我们来使用 LLDB 打开这个日志 要打开崩溃日志 可以辅助点按 这个文件并使用 Xcode 将它打开 然后 Xcode 会询问是否要在 项目的上下文中打开这个文件 我们选择“Destination Video”
现在 Xcode 使用 LLDB 创建了调试会话 其中包含崩溃时的程序状态 崩溃所在的行已高亮显示 表明代码未能打开 某个 JSON 文件 在崩溃发生的前一刻 程序记录了它尝试打开的文件名 所以我可以联系同事 让对方提供日志文件 但程序是如何发生崩溃的? 调试器提供了一个工具 来回答这个问题 即 回溯栈跟踪 回溯栈跟踪描述了 导致这个程序状态的 函数调用序列 也称为栈帧 它可以让我们了解每个函数的作用 它们是在哪里被调用的 以及每个函数要返回到哪里
我们可以在 Xcode 的调试 导航器中找到当前的回溯栈跟踪 那么 程序崩溃时发生了什么? 当前帧用于调用 JSON 载入函数 之前的帧显示正在导入视频元数据 这发生在程序初始化期间
无论是搭配崩溃日志使用 还是在常规调试会话中使用 回溯栈跟踪都是了解 程序控制流的强大工具 搭配崩溃日志使用时 回溯栈跟踪可帮助我们 直观地了解崩溃是如何发生的 在我们的示例中 崩溃日志还帮助我们识别了 可能由程序记录的信息 从而提供了另一种调查问题的方法 要通过崩溃日志 获取正确的行号信息 请确保项目 与创建崩溃日志的 App 版本 在同一个 commit 上签出 并且可以使用相应 构建版本的 dSYM 捆绑包 我们在“符号化:超越基础功能”中 更详细地介绍了 dSYM 捆绑包 “Destination Video”使用 SwiftUI 构建而成 我想了解更多相关信息 我设计了一个功能原型 可让用户将 视频添加到“Watch Later”列表中 我的目标是了解某些 代码行会在什么情况下执行 我们来探索如何借助 LLDB 和断点来实现这个目标 在 App 的主屏幕上 如果我们选择一个视频 可以看到 视频内容的 DetailView
在这个视图中 可以找到我创建的 “Add to Watch Later”按钮 当我点按这个按钮时 它的文本会发生变化
为了帮助我了解它的运作方式 我将在创建这个按钮的 位置创建一个断点 这是我进行原型设计的代码 其中包含对 Button 类的 构造函数调用 我们来点按相关行号 设置一个断点
新断点现在显示在断点导航器中 不过 请注意应用程序 启动后发生了什么 LLDB 将行断点 解析成了三个不同的位置
这表明我们可以 通过不同的代码路径 在这个断点处暂停 让我们再次前往视频的 DetailView 验证一下这个假设
调试器停止了程序 Xcode 高亮显示了 程序停止时所在的行 可以看到它即将调用 Button 的构造函数 通过检查回溯栈跟踪 可以确认程序正处于用来 构造 UI 元素的嵌套调用之中
例如 这个帧用于创建 元素的垂直堆栈 我们来看看前一个帧 包含什么内容
这个帧用于创建 ScrollView
我们已经在一个断点处停了下来 但 LLDB 一共识别出三个 与这一行相关的位置 我们可以使用“breakpoint list” 命令获取有关断点的更多信息 它不仅描述了我们 在第 70 行设置的断点 还使用行号和列号 描述了与这个断点相关的 三个位置 列表中的第一个位置 是程序当前停止的位置 即第 70 行上 对 Button 构造函数的调用 我们可以从行和列信息中 观察到这一点 但也可以使用 断点标识符 也就是 ID
LLDB 会为每个断点位置 分配一个 ID 在本例中是 1.1 这与 Xcode 高亮显示 断点行时使用的标识符相同 列表中第二个 断点的标识符为 1.2 指的是构造函数的 第一个参数 action 闭包 这个断点应该会在我们 点按按钮时触发 最后一个断点位置的 标识符为 1.3 指的是 构造函数调用中的后置闭包 尽管闭包主体 以上一行中的花括号开头 但这个位置 实际上解析为下一行 我们来尝试到达这些断点 在这个初始构造函数调用处 我们继续执行程序
程序在后置闭包内停止 断点标识符为 1.3 通过使用回溯栈跟踪 我们发现 构造函数是之前的一帧
也就是说 构造函数本身 调用了这个闭包
要到达最后的断点 必须点按“Add to Watch Later”
程序再次停止 但这次停在了 action 闭包内 这个示例说明 即使是最基本的断点 也可能 会让程序以有趣的方式停止 我们有三个归因于 同一行的不同代码区域 它们可通过不同的代码路径到达 这些路径分别是 对构造函数的调用、 由构造函数本身调用的后置闭包 以及只有在点按按钮时 才会调用的 action 闭包 在 SwiftUI 之类的大量 使用闭包的声明式代码中 这种情况很常见 我们并不总能知道 程序何时会调用闭包 因此 在闭包主体内设置行断点 可以有效地在调用闭包时暂停程序 暂停应用程序 是调试周期的重要一环 但我们可以结合使用 程序暂停和程序检查 来改善调试体验 例如 我们来尝试了解 UI 元素如何与程序交互 并重点关注第一个断点 也就是 调用 Button 构造函数的断点 为了只在创建按钮时暂停 让我们停用最后两个断点位置
现在来点按按钮 这应该会触发 UI 更新 和我们的断点
借助 LLDB 我们可以使用“p”命令 检查“Watch Later”列表的大小 稍后我们将更详细地讨论这个命令
列表中有一个视频 就是我们刚刚添加的 我们甚至可以检查它的标题
在“暂停/检查”调试周期中 多次重复运行相同的命令会很繁琐 利用断点操作这一概念 可以让调试器在到达断点时 运行命令 帮助我们进行调试 下面我们来更改示例 让调试器在每次到达断点时 打印“Watch Later”列表的条目 可以通过辅助点按某一断点 来找到“Edit Breakpoint”菜单 我们来添加 “Debugger Command”操作 如果列表中存在最近添加的视频 就将视频名称打印出来
我们甚至可以在到达 断点后继续执行程序
现在 每次调用构造函数时 我们都能获得有关队列的信息
通过这种方式 无需重新编译代码 即可利用调试器打印信息 到目前为止 我们一直在使用 Xcode 的 图形用户界面与调试器交互 但我们也可以使用 LLDB 提供的丰富的命令行界面 让我们重复上一个 示例中的步骤 但这次使用命令行来完成 要访问“Debugger Command”行 首先暂停应用程序
现在 我们可以用 “b”命令设置一个断点 这个命令可以替代更宽泛的 “breakpoint set”命令 并且更简短
这个命令行也可用于添加断点操作 但请注意 这会覆盖 通过 Xcode 设置的所有操作 我们使用“break command add” 来打印最后一个添加到 “Watch Later”列表的视频的名称
和之前一样 程序在打印之后继续执行 这个命令会影响最新创建的断点 但如果提供了可选的 断点标识符参数 它也可以修改不同的断点
通过使用 help 命令 LLDB 可以提供关于它的 所有命令的详细描述 你还可以获取有关 特定命令的任意选项的帮助 要探索 LLDB 的更多功能 apropros 命令是个很棒的工具 这个命令可以在 LLDB 的 帮助文本中搜索关键词 并返回相应关键词 描述的所有命令或选项 例如 搜索与 backtrace 相关的命令时 会找到 frame select 以及它的别名 即 f 命令 还会找到 thread backtrace 命令 在调试时 我们创建的断点 经常会被多次触发 但我们只关心 其中的一部分触发结果 一个常见的示例是 当我们将断点放置在循环内时 我们不希望在每次迭代时暂停 而是只想在发生特定事件时暂停 我们来了解一下可用于处理 高触发频次断点的三种主要技巧 我们来看这个代码片段 它对一个集合中的视频进行迭代 在视频处于远程位置时 载入视频并进行处理 我们只希望在视频很长的情况下 在 loadRemoteMedia 函数处停止 为了实现这一点 我们可以设置一个行断点 并使用断点条件对它进行修改 断点条件定义了一个规则 用于决定调试器是否应该 停止运行程序 在命令行中 我们可以使用 break modify 命令 为命令提供断点 ID 和条件 断点位置上的任何有效代码 都可以用作条件表达式 在示例中 我们可以修改断点 仅在当前视频长度 超过 60 秒的情况下 才会停止程序 在 Xcode 中可以辅助点按断点 前往“Edit Breakpoint” 并填充“Condition”字段 回到我们的示例 我们希望仅在执行了 loadRemoteMedia 函数的情况下 在调用 processVideo 时停止程序 为了实现这一点 我们可以再次设置行断点 但这次添加一个断点操作 我们之前曾使用 断点操作来打印变量 但它们也可以用于创建新的断点 通过使用“tbreak”命令 我们可以创建一个临时断点 让程序仅在这个位置暂停一次 在示例中 可以在 loadRemoteMedia 上 设置一个自动继续执行断点 并添加一项操作 在 processVideo 上创建一个临时断点
第三种技巧会以固定次数忽略断点 并在之后到达断点时停止程序 例如 我们可以忽略 集合中的前 10 个视频 为此 需要像之前一样修改断点 但这次我们使用 --ignore-count 标志 在 Xcode 中 “Edit Breakpoint” 界面上也有相同的选项 在极端情况下 当一行代码 需要执行数百万次时 之前介绍的这些技巧可能会 显著减慢程序执行速度 因为调试器仍然 每次都需要停止程序 并决定是否继续执行 在这种情况下 建议重新编译代码 例如 我们可以计算停止条件 并在 if 语句中设置一个断点 仅在条件为 true 时执行暂停 一个巧妙的方法是将 raise 函数 与 SIGSTOP 信号搭配使用 这可以指示应用程序停止运行 如果你通过 Xcode 或 LLDB 运行程序 调试器会将程序视为 到达断点并接管程序 到目前为止 我们重点介绍了 调试周期的两个组成部分 即 启动程序 以及在需要 关注的位置暂停 但我们只是简单提及了 用于检查程序状态的主要工具 即“p”命令 在上一个示例中 我们使用“p”命令 作为查看变量 和计算表达式的主要方式 LLDB 提供了许多其他命令 来实现这一操作 它们都有各自的用途 了解所有这些工具 可能会让人倍感压力 但从 Xcode 15 开始 如果你需要检查某一变量 或计算某一表达式 那么“p”命令 将适用于大多数这样的情况 它经过重新设计 现在是 dwim-print 命令的别名 它将许多不同的工具 整合到一条命令中 可帮助你节省时间 我们在“使用结构化日志 进行调试”中进行了详细介绍 我们来试试这个命令 我尝试在 App 中添加一个新视频 为此 我编辑了视频的 JSON 描述 但是 我一启动 App 它就崩溃了
这是我为新视频编辑的 JSON 文件 我们来启动 App 以便调试器在崩溃时停止程序
通过检查控制台 我们注意到 “Destination Video”的开发者 在工作流程中使用了日志记录功能
记录的最后一条信息显示 程序正在载入视频的 JSON 文件
从程序启动到报告异常的 这段时间出现了问题 这时 首选方法是在调用 JSON 载入函数之前 设置一个断点 看看是否能帮助我们 找到错误的蛛丝马迹 我们来试一试
通过辅助点按日志信息 可以快速前往相应的源代码位置
我们在这个函数末尾设置一个断点
我们需要在前一个时间点暂停 因此需要重新启动程序 我们没有修改代码 因此可以 按住 Control 键并点按启动 无需重新编译代码即可 重新启动程序 从而节省时间
我们来看看 URL 和文件名 这两个局部变量
它们看起来都没问题 我们还可以在变量 查看器中直观地查看它们
或者直接将鼠标 悬停在它们的源代码上
有些类型甚至还有快速查看按钮 点按后可提供变量的更多详细信息
这看起来像是我编辑的文件
我们检查了 JSON 载入函数 它看起来正确无误 那么错误可能位于 这个函数之后的某个地方 我们来看看程序的另一个部分 即 采用 JSON 解码器的 Video 构造函数
我怀疑其中某个 try 语句 有问题 但如果我们每次 调用 Video 构造函数时都暂停 就有一个问题
那就是应用程序中的视频太多了 幸好 我们前面介绍了一些技巧 可用于处理这类高触发频次的断点 我知道新视频是 App 中的第 12 个视频 所以可以向这个断点应用 “--ignore-count” 不过 我们来尝试一种新技巧 即 Swift Error 断点 一旦抛出 Swift Error 这种类型的断点会指示 LLDB 停止运行应用程序
我们点按继续执行程序
程序在尝试解码 imageName JSON 键时 在 Error 断点处停止了 说明这里出现了问题 我们使用回溯栈跟踪功能 前往输入数据所在的 之前的帧
作为程序员 我喜欢用代码 解决这类问题 而“p”命令 给了我很多自由操作的空间 我们来编写一些代码 弄清楚这个“data”数组中 有多少个 imageName
哇 看来我们需要创建一个字符串
输出内容太多了 我们来搜索“imageName”
范围缩小了 但输出还是太多了 我们来检查一下 count 属性
我本应该有 13 个视频 但只有 12 个名为“imageName”的键 说明有问题 我们来检查一下 JSON 文件
原来是我在输入 imageName 时打错字了 我来改一下
这就没问题了
通过这个示例 我们了解了如何使用“p”命令 来检查变量以及计算复杂的表达式 它可以在回溯栈跟踪的 任何帧中执行这些操作 我们可以逐步构建复杂的表达式 无需重新编译任何代码 即可在构建过程的 每一步打印中间结果 这个调试会话还让我们深入了解到 可以通过在 JSON 文件中 添加缺少的键 来改进应用程序的日志记录 到目前为止 我们检查的 大多数变量都相当简单 但在调试会话期间 包含过多数据的变量类型 检查起来非常繁琐 而在变量查看器或“p”命令中 提供简短的说明 或许可以简化检查 这些变量类型通常都有许多属性 或经常存储在集合内 例如 在变量查看器中 WatchLaterItem 集合 不会显示有关每个条目的任何信息 除非手动将它们展开 一直以来 LLDB 都提供一种机制 用于自定“p”命令 和变量查看器的输出 而 Swift 6 引入了一种机制 可以使用新的 @DebugDescription 宏 直接从源代码中自定输出 除了使用这个宏来标注类型 我们还必须创建 一个用于概述类型的 DebugDescription 字符串属性 这个属性必须使用字符串插值 和存储的属性来创建 我们来为 WatchLaterItem 类型 实现这个属性 这个数据结构包含 三个相关的数据成员 分别是视频、名称以及将视频 添加到列表的日期 我们先使用 DebugDescription 宏 标记这个类型 然后创建 debugDescription 字符串计算属性 在这个示例中 我们将使用 name 和 addedOn 日期 作为类型的摘要 这时 如果我们再次检查变量查看器 会发现摘要显示在集合的每项条目中
如果你之前使用过 CustomDebugStringConvertible 协议 可能会觉得这个示例很熟悉 你在使用这个协议时 可能会 使用“po”命令来打印类型 在这些情况下 请检查协议的实现情况 如果协议只是使用 字符串插值和计算属性 那么你可以采用宏而不是协议 这些类型可以更好地与调试器整合 这样一来 你在调试时就可以 重点使用单个命令即“p” 我们现在已经多次经历了调试周期 探索了有效使用调试器 需要了解的主要概念 我们讨论了如何像处理 搜索问题一样处理调试 LLDB 这款强大的工具 提供条件断点和变量检查功能 可以有效地执行这类搜索 它还是帮助我们了解 不熟悉的代码库的绝佳工具 通过断点操作和表达式求值 我们可以利用编程技能 在调试时执行代码 而且 无需重新编译项目 即可执行代码 希望大家可以利用 今天介绍的技巧更快找到错误 找到导致错误的根本原因后 别忘了为相应场景添加测试覆盖范围 你之前可能会遗漏这项操作 感谢观看
-
-
8:09 - WatchLater button
Button(action: { watchLater.toggle(video: video) }) { let inList = watchLater.isInList(video: video) Label(inList ? "In Watch Later" : "Add to Watch Later", systemImage: inList ? "checkmark" : "plus") }
-
12:54 - Printing watch later list information
p watchLater.count p watchLater.last!.name
-
13:45 - Breakpoint actions: Printing name of the most recently added video
p "last video is \(watchLater.last?.name)"
-
14:42 - Breakpoint actions: on the command line
b DetailView.swift:70 break command add p "last video is \(watchLater.last?.name)" continue DONE
-
26:46 - @DebugDescriptio macro example
// Type summaries @DebugDescription struct WatchLaterItem { let video: Video let name: String let addedOn: Date var debugDescription: String { "\(name) - \(addedOn)" } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。