Advanced Debugging with Xcode and LLDB

Posted by Phillip Song on 2019-08-12

Swift Debugging Reliability (提升 Swift 调试可用性)

从 AST context 获取模块失败的问题

在 Xcode 10 中,为了应对这个问题,会为当前的 frame 调用栈创建一个新的 expression evaluator。

412_hd_advanced_debugging_with_xcode_and_lldb_ast_context

Swift Type Resolution (解决 Swift 类型问题)

还有一些开发者会遇到在调试的时候无法显示变量类型,打印变量信息的问题,如下图:

412_hd_advanced_debugging_with_xcode_and_lldb_swift_type_resolution

Apple 针对大量的错误报告进行追踪,在 Xcode 10 中修复了这个 bug,调试信息中将不再会出现此类错误。

一些高级调试技巧 Advanced Debugging Tips and Tricks

配置专用的调试标签页

在 Xcode 10 中可以设置在触发断点的时候自动新建一个新的 Debug 标签页,通过切换标签页你可以快速的切回到之前工作的页面。

412_hd_advanced_debugging_with_xcode_and_lldb-0003.png

通过 LLDB 表达式修改程序状态

LLDB 中通过 expression 命令可以修改程序当前的各种状态,如改变变量的值或者执行一个方法。eexpr 作为简写也可以实现同样的功能。

412_hd_advanced_debugging_with_xcode_and_lldb-0001.png

可以看到 didReachSelectedHeightexpression 语句执行后,被成功改到了 false

利用断点和调试命令实时插入代码

除了直接在调试控制台通过 LLDB 修改 App 状态,我们还可以通过在断点中添加命令来实现相同的功能,并且通过断点来设置调试命令的方式更加方便实用,不用重复手动执行 expression 命令。

设置一个断点,双击或者右键 Edit Breakpoint 打开编辑框,你可以将多个不同的调试命令按顺序填入 Action 中,就能实现之前同样的功能。另外你可以勾选 Automatically continue after evaluationg actions,可以自动继续执行后续代码,而不会停在这一行。

412_hd_advanced_debugging_with_xcode_and_lldb-0002.png

我们也可以通过这种方法,不用重新编译运行 App 就可以查看修改后的效果,如下:

412_hd_advanced_debugging_with_xcode_and_lldb-0004.png

在确认修改正确后,我们就可以将修改应用到代码中。

在汇编调用栈中打印函数实参

当前 App 中的 UILabel 内容显示不正确,如果我们想要找到修改这个 UILabel 的代码并查看其中的具体逻辑时,我们应该怎么办呢?

首先,我们了解一下全局断点,你可以点击在 Breakpoint Navigator 左下角 + 号,然后选择 Symbolic Breakpoint..., 你可以在 Symbol 中输入任何你想监听的函数,如 [UILabel setText:], 之后所有页面下的所有 UILabel 类型对象在设置 text 属性的时候都会执行该断点。

这里 Symbol 中输入的是 Objective-C 的格式,因为 UIKit 是用 Objective-C 实现的。

412_hd_advanced_debugging_with_xcode_and_lldb-0005.png

如下图所示,在 Breakpoint 的下方,显示的是来自调试器的反馈,表明它能够将此断点解析到 UIKit Core 中的某个位置。 某些符号可能解析到多个位置,同样你可以在这里看到它们。如果你没有在这里看到任何条目,表明调试器无法解析你设置的断点,因此断点不会被触发。

412_hd_advanced_debugging_with_xcode_and_lldb-0006.png

设置好断点后继续相关操作,就可以看到触发了 [UILabel setText:] 的断点。

412_hd_advanced_debugging_with_xcode_and_lldb-0007.png

我们并不能查看 UIKit 的源代码,因此在这里,只能看到相关的汇编代码,但即使在一个系统框架的汇编代码中,我们也可以检查传递给函数的参数。只需要知道在这个架构中的调用约定,你就可以检查寄存器以查看参数。我们不需要记住这些寄存器,因为调试器给我们提供了 “伪寄存器” (pseudo-registers)。

如下图,$arg1 被转换成寄存器保存的第一个参数,这里保存的是当前 UILabel 实例对象。

如果你对 Objective-C 的消息发送机制比较熟悉的话,你应该知道第二个参数应该是 selector。我们这里并没有看到具体的对象,这是因为 LLDB 并不知道这些参数的具体类型。
所以在某些情况下,我们需要对其进行类型转换,之后我们就可以看到此消息的选择器。

$arg3 则是传递给 $arg2 函数的第一个参数,这里即是 setText 的第一个参数。

412_hd_advanced_debugging_with_xcode_and_lldb-0011.png

有一些不方便的是,这个断点会在所有调用 [UILabel setText:] 时被触发,是我们没法精确找到需要的出发的位置。因此,我们可以在编辑断点时添加触发的条件,只在 condition 中语句返回 true 时触发。如下:
412_hd_advanced_debugging_with_xcode_and_lldb-0012.png

利用 “breakpoint set –one-shot true” 命令创建一次性断点

上面我们介绍了全局断点,它能检测到全局的函数调用,如果想监测某一个函数内局部区域的函数调用,这个时候我们可以使用 breakpoint set --one-shot true 命令动态生成一个断点,这个断点将是一次性的,只有在触发之后才会存在,之后会自动删除。

412_hd_advanced_debugging_with_xcode_and_lldb-0013.png

这个断点并不会导致命令执行暂停,它只干了一件事,就是通过命令 breakpoint set --name "[UILabel setText:]" 创建了一个全局断点,加上 --one-shot true 就代表是一次性的断点。

如上图的情况, 会在我们设置断点的位置创建一个临时的断点,然后继续执行下面的语句。所以之后只会在调用当前函数后触发设置的 [[UILabel setText:]] 断点, 可以帮助我们更准确的找到调用的位置。

找到错误调用的位置后,我们同样可以使用类似的方式来验证修改。如下图,在下一行新建一个断点来设置新的 valueText, 并且勾选在执行断点语句后自动运行。

412_hd_advanced_debugging_with_xcode_and_lldb-0014.png

通过拖拽或 “thread jupm –by 1” 命令跳过一行代码

如下图,在调试过程中想直接跳过动画执行的过程,可以在断点触发的位置,直接拖拽红色箭头指向的按钮,拖到哪从哪里开始执行,往上拖可以重复执行之前的代码,往下拖将不执行中间被跳过的代码。

412_hd_advanced_debugging_with_xcode_and_lldb-0015.png

在跳过之后,expression jumpAstronaut(animated: false) 即可。

而当我们想每次经过 jumpAstronaut(animated: true) 时,都自动跳过,就可以使用 thread jump --by 1 来设定经过断点时跳过一行代码, 并且可以同时添加多个 action 来实现后续的操作。
412_hd_advanced_debugging_with_xcode_and_lldb-0017.png

利用 watchpoints 监听变量的变化

我们可以使用 watchpoints 通过监测内存的变化来监听属性的变化。

如下图,我们想要监听 GamePlay 中的 attempts 属性,选中想要监听的属性后,点击右键后将弹出窗口,选择 Watch "attempts" 即可监听属性 attempts 的值的改变。需要注意的是,每当重新编译后指针会发生变化,需要重新设置 watchpoints。

412_hd_advanced_debugging_with_xcode_and_lldb-0018.png

同样,在找到改变属性变化的位置时,同样可以通过在断点中使用 expression if attempts >= maxAttempts { state = .end } 语句来验证我们的代码修改。

412_hd_advanced_debugging_with_xcode_and_lldb-0019.png

Swift 调用栈中在 LLDB 调试器使用 Obj-C 代码命令

在日常调试中,使用 LLDB 命令 po [self.view recursiveDescription] 命令来输出页面视图结构是非常方便的,然而我们在 Swift 调用栈中使用这个命令的时候将打印以下错误:

1
2
3
4
po self.view.recursiveDescription()
error: <EXPR>:3:6: error: value of type 'UIView?' has no member 'recursiveDescription'
self.view.recursiveDescription()
~~~~~^~~~ ~~~~~~~~~~~~~~~~~~~~

其实我们可以通过 expression -l objc -O -- 命令来使用 Obj-C 代码来输出我们想要的视图结构,记得 self.view 两边一定要加上 ` 符号。

1
expression -l objc -O -- [`self.view` recursiveDescription]

不知道你们有没有觉得上面这个命令有点长,还好我们可以通过 command alias <alias name> expression -l objc -O —- 为这句命令建立一个别名,之后就可以通过别名来使用相关操作。

412_hd_advanced_debugging_with_xcode_and_lldb-0020.png

我们也可以使用 po unsafeBitCast(<pstr> , UnsafePointer.self) 命令打印对象描述,而在这里 unsafeBitCast 的优点在于它返回了对应类型的结果,因此我们可以在其上调用我们的函数和属性名称。

412_hd_advanced_debugging_with_xcode_and_lldb-0021.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(lldb) po unsafeBitCast(0x7fb3afc3f480, to: ScoreboardView.self)
<Solar_System_iOS.ScoreboardView: 0x7fb3afc3f480; frame = (0 0; 113.667 65); layer = <CALayer: 0x6000018118a0>>

// 打印frame
(lldb) po unsafeBitCast(0x7fb3afc3f480, ScoreboardView.self).frame
▿ (0.0, 0.0, 113.66666666669 65.0)
▿ origin : (0.0, 0.0)
- x : 0.0
- y : 0.0
▿ origin :
.....

// 打印中心点坐标
(lldb) po unsafeBitCast(0x7fb3afc3f480, ScoreboardView.self).center

// 设置中心点坐标
(lldb) po unsafeBitCast(0x7fb3afc3f480, ScoreboardView.self).center.y = 300

而在上述的例子中,通过 unsafeBitCast 修改了 UI 的坐标值,但是页面不会发生变化,你需要的只是使用 expression CATransaction.flush() 来刷新下你的页面。

1
2
3
(lldb) po unsafeBitCast(0x7fb3afc3f480, ScoreboardView.self).center.y = 300

(lldb) expression CATransaction.flush()

利用命令别名和脚本添加自定义 LLDB 命令

当你对 LLDB 命令越来越了解,操作越来越骚的时候,你会发现小小的控制台会限制你的发挥,这个时候你需要一个更大的舞台。
现在我要展示如何使用 Python 脚本执行命令,你需要先下载一 个 nudge.py ,这是苹果开发工程师为我们准备好的 Python 脚本,它可以帮助我们简单、快速地移动 UI 控件。我们需要将 nudge.py 文件放入你的用户根目录~/nudge.py。

下一步我们需要在用户根目录下新建一个 ~/.lldbinit 文件,并加入下方命令和别名:

1
2
3
command script import ~/nudge.py
command alias poc expression -l objc -O --
command alias 🚽 expression -l objc -- (void)[CATransaction flush]

做完这些,我们就可以来使用我们的自定义命令 nudge x-offset y-offset [view] 了,具体用法如下:

1
2
3
4
5
6
7
8
9
10
11
// 引用 nudge
(lldb) command script import ~/nudge.py
The "nudge" command has been installed, type "help nudge" for detailed help.

// 拿到对象指针
(lldb) po myLabel
Optional<UILabel>
- some : <UILabel: 0x7fc04a60fff0; frame = (57 141; 42 21); text = 'Label'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600001d36c10>>

// Y轴向上偏移5
(lldb) nudge 0 -5 0x7fc04a60fff0

调整模拟器中控件位置的效果:

412_hd_advanced_debugging_with_xcode_and_lldb_nudge

LLDB 打印命令

Screen Shot 2019-08-18 at 11.34.53 PM.png

ppo 命令从别名和执行过程上来看,分别输出的是对象和 LLDB 格式数据。
frame variable 不同之处的是从当前 frame 调用栈的内存中拿到的值。只接受变量作为参数,不接受表达式。通过 frame variable 命令,可以打印出当前 frame 调用栈的的所有变量。

深入了解 Xcode 视图调试技巧

在调试导航栏中快速定位视图位置

在开发中,我们会频繁使用到 Debug View Hierarchy 查看当前页面视图结构,正常情况下导航栏的 UI 嵌套层级会非常多,让我们无法快速准确找到我们想查看的控件所在的层级。

其实 Xcode 中已经有快捷方式可以让你快速定位到控件在导航栏中的位置,首先选中你需要查看的控件,然后在导航栏中的 Navigate 选项,展开后选择 Reveal in Debug Navigator, 如下图:

412_hd_advanced_debugging_with_xcode_and_lldb-0022.png

在调试中查看自动布局信息

在 Debug View Hierarchy 中查看控件的约束只需要启动 Show Contraints 选项,选中任何一个控件都会显示出其拥有的约束。(下图红色箭头指向的即是 Show Contraints)

412_hd_advanced_debugging_with_xcode_and_lldb-0024.png

显示被裁剪的视图内容

上图你可能注意到右上方视图的 constraints 有一点奇怪,导致视图没有显示完整,超出页面的部分被剪裁掉了。 这个时候我们需要确认下事实是否如此,我们可以通过开启 Show Clipped Content 选项来进行验证。如下图,红色箭头指向的即是 Show Clipped Content 选项。

412_hd_advanced_debugging_with_xcode_and_lldb-0025.png

同样,我们在这里可以直接通过 LLDB 来修改错误的约束。

在视图结构中选择想要修改的视图约束, 然后在导航栏中的 Edit 中选择 Copy, 打开 LLDB 调试控制台,如下图所示:

412_hd_advanced_debugging_with_xcode_and_lldb-0026.png

跟之前的情况相同,这里同样需要使用 expression CATransaction.flush() 来更新 UI。

在调试检查器中显示调用栈

在调试模式下,我们有办法看到每一个控件,每一个约束的创建调用栈,方便我们快速定位到问题的源头。

在打开 Show Contraints 选项后,你可以找到想要查看的约束并选中,在右边栏的对象检查器的 Backtrace 一栏你可以看到一个调用栈的列表。 如下图,点击右边小箭头就可以跳转到创建该对象的代码处。

412_hd_advanced_debugging_with_xcode_and_lldb-0029.png

这项功能是需要手动开启的,你可以通过点击项目 Target -> Edit Scheme.. -> Run -> Diagnostics -> Logging -> 勾选 Malloc Stack 并且切换至 All Allocation and Free History 模式开启此功能。

412_hd_advanced_debugging_with_xcode_and_lldb-0028.png

切换深色模式

在 macOS 10.14 版本下并且安装了 Xcode 10 ,你就可以在开发中使用 Dark Mode 了,你可以在 Xcode 底部的找到一个黑白两色小方块按钮,通过选中这个按钮,你可以切换模拟器 Dark 和 Light 两种外观。如果你的 Macbook 有 Touch Bar 的话,你也可以通过 Touch Bar 上的按钮来切换。

412_hd_advanced_debugging_with_xcode_and_lldb-0030.png

在 StoryBoard 中你可以在底部找到 View as : Light/Dark Appearance 来预览 Dark 和 Light 外观。
macOS 开发中选中任意一个 View ,你都可以在右边栏的检查器中找到 Appearance 属性,通过这个属性你可以为这个 View 及其子视图设置固定的外观颜色,且不会随着用户切换 Dark 和 Light 外观而改变颜色。

在检查器中查看深色模式信息

在 UI Hierarchy 调试中我们可以在右边栏的检查器中查看 Dark Mode 相关信息,选中一个 UILabel 可以查看该 label 的 Text Color 属性。在 Dark Mode 下一共有 3 中类型颜色:

  • System Color: 系统推荐颜色 System Color ,可以根据当前外观颜色自适应文字颜色。

  • Named Color:Named Color 需要开发者在 assets catalog 中设置,可以针对 Dark Light 设置不同色值。

  • 自定义 RGB 颜色:纯手动设置的自定义 RGB 固定色值。

下图中 detailTitleColor 就是在 asset catalog 中定义的 Named Color。
412_hd_advanced_debugging_with_xcode_and_lldb-0031.png

总结

强大的 LLDB 配合断点使用,让我们的调试手段更加的灵活,掌握这些调试的方法,我们不需要重新编译运行 App 就可以轻松实现修改和验证,减少 Debug 所需要的时间,可以极大的方便我们的开发过程。