WWDC 2018:效率提高爆表的 Xcode 和 LLDB 调试技巧

WWDC 2018 Session 412 : Advanced Debugging with Xcode and LLDB程序员

前言

在程序员写 bug 的职业生涯中,只有 bug 会永远陪伴着你,如何处理与 bug 之间的关系,是每一位程序员的必修课。特别是入门程序员常常受 bug 的影响,熬夜加班压力大,长痘长胖还脱发。express

每一位 iOS 和 macOS 开发者都是幸运的,由于苹果的 Xcode 和 LLDB 调试工具,这是每一位开发者应该使用的调试神器,能够帮助咱们更快地解决问题。本文将主要讲解 Xcode 的 断点调试LLDB 调试器 以及 视图结构调试(UI Hierarchy)的使用技巧,这些技巧将大幅减小调试中从新编译的次数,减小你的等待时间。xcode

这些技巧使用起来很是简单,并且在开发场景很是实用,每一位开发者都有必要掌握这些技巧。bash

1、提高 Swift 调试可用性 (Swift Debugging Reliability)

1.1 解决从 AST context 获取模块失败问题(Failed to get module from AST context)

相信不少开发者在使用 Swift 的时候,调试过程当中的一些问题会让你很头痛。 好比说下面这个问题,LLDB 在 AST Context 重建编译状态时,有些时候在复杂的状况下可能没法检测到部分模块的变化,因而调试器提示Failed to get module from AST contextapp

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

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

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

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

2、吐血推荐的调试小技巧(Advanced Debugging Tips and Tricks)

2.1 自动建立调试标签页(Configure behaviors to dedicate a tab for debugging)

想必你常常在看代码的时候因为执行到断点而被强行切换到断点所在的页面,在断点页面和以前页面进行切换的体验是很是差的。如今你能够设置在被断点的时候自动新建一个标签页,经过切换标签页你能够快速便捷地切回到以前浏览的页面。布局

设置自动新建 Debug Tab 方法:顶部导航栏 Xcode -> Behaviors -> Edit Behaviors... -> Runing -> Pauses -> ✅ Show Tab Name tab name in active windowpost

2.2 在 LLDB 中修改 App 状态(LLDB expressions can modify program state)

在 LLDB 中经过expression命令能够改变程序当前的各类状态,eexpr 做为简写也能够实现一样的功能。咱们用一个简单的UILabel来举例,为myLabel设置一个值 hello , 正常来说视图上的myLabel就应该显示 hello 。

func test() -> Void {
    myLabel.text = "hello"
// 断点 -> 
}
复制代码

你能够在myLabel.text = "hello"这句代码后设置一个断点,运行程序执行断点后,在控制台的 LLDB 调试器 中输入下面的表达式改变它的值,在继续运行程序以后,相信你在界面上看到的值必定是 hello world 。

// 改变 myLabel 文案
expr myLabel.text = "hello world"
复制代码

除了改变myLabel.text的值以外,你能够像在 Xcode 中写代码同样,在 LLDB 中进行一样的操做。例如你能够像下面的代码同样使用表达式改变它的文字颜色,也能够执行某个函数。

// 改变 myLabel 文字颜色
expr myLabel.textColor = UIColor.red

// 执行 test 方法
expr test()
复制代码

2.3 利用断点实时插入代码(Use auto-continuing breakpoints with debugger commands to inject code live)

除了直接在控制台经过 LLDB 调试器修改 App 状态,你还能够经过在断点中添加命令来实现一样的功能。并且经过断点来设置调试命令的方式更加方便实用,几乎是实时插入代码的功能。

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

2.4 在汇编调用栈中打印函数实参("po $arg1" ($arg2, etc) in assembly frames to print function arguments)

首先,咱们了解一下全局断点,你能够点击在 Breakpoints Navigator 左下角 + 号,而后选择 Symbolic Breakpoint... ,以下图,你能够在 Symbol 一栏输入任何你想监听的函数好比[UILabel setText:],以后全部页面下的全部UILabel类型对象在设置text属性的时候都会执行该断点。(ps:我还不是最酷的😎)

在这个断点的控制台中,并无显示变量属性等信息,咱们怎么能知道设置了什么呢?接下来咱们能够用$arg1$arg2等命令来打印出咱们想要的信息。

以下图,在这里$arg1是指对象自己,$arg2是对象被调用的函数,po命令没法直接输出函数名,须要加上(SEL)$arg3是被赋给text的值。

2.5 利用 “breakpoint set --one-shot true” 命令建立一次性断点(Create dependent breakpoints using )

上面咱们介绍了全局断点,它能监测到全局的函数调用,可是我想监测某一个函数内局部区域的函数调用,这个时候咱们可使用breakpoint set --one-shot true命令动态生成一个断点,这个断点将是一次性的,执行一次后将被自动删除。

最酷的是,咱们将建立会先一个断点,以下图,让这个断点来实现这一切,即用一个断点来建立另一个一次性的断点,为了让整个过程是无感的,我建议勾选 Automatically continue after evaluationg actions 选项。

上图这个断点到底干了什么?当执行到图中第 61 行的断点时,这个断点并不会致使命令执行暂停,它只干了一件事,就是经过命令breakpoint set --name "[UILabel setText:]"建立了一个全局断点,加上--one-shot true就表明是一次性的断点。

如上图的执行效果就是breakpoint set --one-shot true --name "[UILabel setText:]"命令会让指针在myLabel.text = "hello"这一行暂停,暂停后一次性的使命就已经结束,因此在下一行myLabel.text = "hello world"是不会暂停的。

2.6 经过拖拽指令指针或 “thread jump --by 1” 命令跳过一行代码(Skip lines of code by dragging Instruction Pointer or “thread jump --by 1” )

首先咱们看如何经过拖拽指令指针来,跳过一段代码不执行。以下图,直接拖拽红色箭头指向的按钮,拖到哪从哪里开始执行,往上拖能够重复执行以前的代码,往下拖将不执行中间被跳过的代码。

咱们经过thread jump --by 2命令,跳过了 2 行代码,以下图将只打印 1 和 4 。

2.7 利用 watchpoints 监听变量的变化(Pause when variables are modified by using watchpoints)

上面咱们介绍了使用全局断点和一次性断点对[UILabel setText:]函数监听属性的变化,其实咱们还有另外一个选择, 使用 watchpoints 经过监测内存的变化来监听属性的变化。

咱们能够在viewDidLoad函数中设置一个断点,而后再控制台找到你须要监听的属性,以下图:

选中你想要监听的属性后,点击右键将弹出下图窗口,点击 Watch "count"便可监听属性 count 的值的改变,如执行count+=1。须要注意的是每当从新编译后指针发生变化,就须要从新设置 watchpoints 。

2.8 Swift 调用栈中在 LLDB 调试器使用 Obj-C 代码命令(Evaluate Obj-C code in Swift frames with “expression -l objc -O -- ”)

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

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两边必定要加上 ` 符号。

expression -l objc -O -- [`self.view` recursiveDescription]
复制代码

不知道大家有没有以为上面这个命令有点长,还好咱们能够能够经过command alias <alias name> expression -l objc -O —- 为这句命令创建一个别名,以后就能够经过别名来使用相关操做。

再另外一种方式,咱们可使用po unsafeBitCast(<pstr> , UnsafePointer.self)命令打印对象描述、中心点坐标,固然也能够设置相关属性。

// 打印对象
(lldb) po unsafeBitCast(0x7fe439d13160, UILabel.self)
<UILabel: 0x7fe439d13160; frame = (57 141; 42 21); text = 'Label'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003942a30>>

// 打印中心点坐标
(lldb) po unsafeBitCast(0x7fe439d13160, UILabel.self).center
▿ (78.0, 151.5)
  - x : 78.0
  - y : 151.5
  
// 设置中心点坐标
(lldb) po unsafeBitCast(0x7fe439d13160, UILabel.self).center.y = 300
复制代码

2.9 利用 “expression CATransaction.flush()” 命令刷新页面(Flush view changes to the screen using “expression CATransaction.flush()”)

你能够在控制台经过 LLDB 调试器中改变 UI 的坐标值,但你并不能当即看到页面有任何改变。事实上你确实修改了它的值,你只是须要使用“expression CATransaction.flush()”来刷新一下你的页面。

配合修改 UI 坐标值的命令一块儿使用,你能看到你的模拟器正在发生使人振奋的一幕。

// 修改坐标点
po unsafeBitCast(0x7fe439d13160, UILabel.self).center.y = 300
// 刷新页面
expression CATransaction.flush()
复制代码

2.10 利用别名和脚本添加自定义 LLDB 命令(Add custom LLDB commands using aliases and scripts)

当你对 LLDB 命令愈来愈了解,操做愈来愈骚的时候,你会发现小小的控制台会限制你的发挥,这个时候你须要一个更大的舞台。

如今我要展现如何使用 Python 脚本执行命令,你须要先下载一 个nudge.py ,这是苹果开发工程师为咱们准备好的 Python 脚本,它能够帮助咱们简单、快速地移动 UI 控件。咱们须要将 nudge.py 文件放入你的用户根目录~/nudge.py

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

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]了,具体用法以下:

// 引用 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
复制代码

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

2.11 LLDB 打印命令(LLDB Print Commands)

Command Alias For Steps TO Evaluate
po <expression> expression --object-description -- <expression> 1. Expression: evaluate
2. Expression: debug description
p expression -- 1. Expression: evaluate
2. Outputs LLDB-formatted description
frame variable none 1. Reads value of from memory
2. Outputs LLDB-formatted description

p 和 po 命令从别名和执行过程上来看,分别输出的是对象和 LLDB 格式数据。

而 frame variable 不一样之处的是从当前 frame 调用栈的内存中拿到的值。只接受变量做为参数,不接受表达式。经过frame variable命令,能够打印出当前 frame 调用栈的的全部变量。

3、深刻了解 Xcode 视图调试技巧(Advanced View Debugging)

3.1 在调试导航栏中快速定位到视图位置(Reveal in Debug Navigator)

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

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

3.2 显示被裁剪的视图内容(View clipped content)

当咱们遇到这样一个显示不全的 bug 的时候,咱们能够用到 Debug View Hierarchy 查看当前视图具体状况,进入调试页面你会看到下面这种状况:

我想个人 label 应该是完整的,可是超出页面被裁剪掉了,这个时候我须要确认一下事实是否是和我想的同样。以下图,咱们须要开启 Show Clipped Content 选项。

最后我看到了真相和我猜想的是一致的,我能够根据真实状况准确制定出解决方案。

3.3 在调试中查看自动布局信息(Auto Layout debugging)

在调试 Debug View Hierarchy 中查看控件的约束只须要启动 Show Constraints 选项,选中任何一个控件都会显示出其拥有的约束。

选中约束后能够在右边栏对象检查器 Object Inspector 中查看约束的详细信息。

3.4 在调试检查器中显示调用栈(Creation backtraces in the inspector)

在调试模式下,咱们有办法看到每个控件,每个约束的建立调用栈,方便咱们快速定位到问题的源头。举个例子,我手动为个人 label 对顶部距离 100 的约束。

let myLabelTopConstraint =  myLabel.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
NSLayoutConstraint.activate([myLabelTopConstraint])
复制代码

运行 Demo 后开启 Debug View Hierarchy ,开启显示约束选项后,你能够找到这个约束并选中,在右边栏的对象检查器的 Backtrace 一栏你能够看到一个调用栈的列表。以下图,点击右边小箭头能够跳转到建立该对象的代码处。

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

3.5 获取对象指针及其拓展(Access object pointers (copy casted expressions) )

在视图调试模式中,咱们有时候也会须要在 LLDB 调试器中输入表达式来达到修改控件位置的的效果。

举例咱们要修改一个约束的值,咱们首先要拿到这个约束对象的指针,好消息是 Xcode 能够很是方便让咱们拿到,选中该约束,直接快捷键 ⌘ + c 就复制好了,能够直接复制到控制台中使用。

你能够输出该约束的描述信息,和右边栏检查器中的 Description 是同样的效果。

// po + 复制好的指针
po ((NSLayoutConstraint *)0x600000dd4460)

// 输出结果
<NSLayoutConstraint:0x600000dd4460 UILabel:0x7fdb1c70a710'WWDC 2018:效率提高爆表的 Xcode 和...'.top == UIView:0x7fdb1c70b950.top + 100   (active)>
复制代码

也许你还须要复习一下以前的内容,来修改一下约束的值,而且刷新页面,完成这些后赶忙看看模拟器的效果。

// 设置约束的值为 200
(lldb) e [((NSLayoutConstraint *)0x600000dd4460) setConstant:200]

// 刷新 UI
// 🚽 是 expression -l objc -- (void)[CATransaction flush] 命令的别名
(lldb) 🚽 
复制代码

3.6 利用快捷键 ⌘-click 选中被遮挡的视图 (⌘-click-through for selection)

在调试中,你要选择的视图被另外一个视图遮挡住的状况下,你能够经过 3D 的查看模式,选中后背的视图,以下图。

可是这种方式实在难称优雅,何况还有一些刁钻的角度会让你很是头疼。在 2D 的状况下,正确的选中方式应该是 ⌘-click 直接选中背后被遮挡的视图,快去试试看吧。

4、调试深色模式(Debugging Dark Mode)

4.1 切换深色模式(Appearance overrides)

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

在 StoryBoard 中你能够在底部找到 View as : Light/Dark Appearance 来预览 Dark 和 Light 外观。

macOS 开发中选中任意一个 View ,你均可以在右边栏的检查器中找到 Appearance 属性,经过这个属性你能够为这个 View 及其子视图设置固定的外观颜色,且不会随着用户切换 Dark 和 Light 外观而改变颜色。

4.2 捕获活动的 Mac app(Capturing active Mac apps)

咱们的 UI Hirerachey 同时只能显示一个 UIWindow 的内容,全部在调试的时候,弹出的 UIWindow 并不会和页面内的 UI 结构一块儿展现给咱们,像 UIAlertView 这种弹出 UIWindow 就没法一块儿显示。

若是咱们须要查看弹出 UIWindow , 咱们须要把左边栏当前的文件结构所有关闭收起,这个时候你会看到 ViewController 所在的 UIWindow 下面还有另一个 UIWindow ,选中以后就能够查看弹出的 UIWindow 的 UI 层级结构了。

4.3 在检查器中查看深色模式信息(Named colors and NSAppearance details in inspector)

在 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 固定色值。

下图中的 Text Color 就是在 assets catalog 中设置的 Named Color ,设置的名字为 titleColor,你能够根据场景为该设置设置合适的名字。

以下图,检查器偏下的位置 View 一栏中,咱们能够找到 Appearance 和 Effective 属性,Appearance 是表示该视图下子视图没法切换的固定的外观颜色选择,Effective 是当前生效的外观颜色。

在 assets catalog 中设置 Named Color:

总结

功能强大的 LLDB ,特别是配合 BreakPoint 一块儿使用,让咱们有了更多的想象空间,加上愈来愈好用的 UI Hirerachey ,让咱们的调试手段更加灵活。 这些内容虽然须要花一些时间去了解,但我相信掌握这些技巧将会为你节省下更多的时间。

今后你不再用为下班前测出 bug 而焦虑了,早用上,早收工,最多干到下午 3 点钟。但愿本文内容对每一位读者有所帮助。

参考连接

查看更多 WWDC 18 相关文章请前往 老司机x知识小集xSwiftGG WWDC 18 专题目录

相关文章
相关标签/搜索