现在你已经学会了如何设置断点,使调试器在你的代码中停下来,现在是时候从你调试的任何软件中获得有用的信息了。
你经常想检查对象的实例变量。但是,你知道你甚至可以通过LLDB执行任意代码吗?更重要的是,通过使用Swift/ObjectiveC的API,你可以在FLY上声明、初始化和注入代码,帮助你理解程序。
在本章中,你将了解到表达式命令。这允许你在调试器中执行任意的代码。
你可能熟悉常用的调试命令,po。po在Swift和Objective-C代码中经常被用来打印出一个感兴趣的项目。这可能是一个对象的实例变量,一个对象的局部引用,或者一个寄存器,正如你在本书前面所看到的。它甚至可以是一个任意的内存引用--只要该地址上有一个对象就可以了!
如果你在LLDB控制台做一个快速的帮助po,你会发现po实际上是表达式-O--的一个速记表达方式。-O语法是用来打印对象的描述的。
po的兄弟姐妹,p,是另一个省略了-O选项的缩写,结果是表达式--。p将打印出来的格式更依赖于LLDB的类型系统。LLDB的值的类型格式化有助于决定它的输出,并且是完全可定制的(正如你将在第二部分看到的)。
现在是时候学习p和po命令如何获得其内容了。在本章中你将继续使用Signals项目。
首先在Xcode中打开Signals项目。接下来,打开MasterViewController.swift,在viewDidLoad()上面添加以下代码。
override var description: String {
return "Yay! debugging " + super.description
}
在viewDidLoad中,在super.viewDidLoad()下面添加以下一行代码。
print("\(self)")
现在,在MasterViewController.swift的viewDidLoad()中创建的print方法之后设置一个断点。使用Xcode GUI的断点侧板来做这件事。
构建并运行该应用程序。
一旦Signals项目在viewDidLoad()处停止,在LLDB控制台输入以下内容。
(lldb) po self
你会得到类似于以下的输出。
Yay! debugging <Signals.MasterViewController: 0x7f8a0ac06b70>
注意打印语句的输出,以及它与你刚才在调试器中执行的po self的匹配情况。
你还可以更进一步。NSObject有一个额外的用于调试的方法描述,叫做debugDescription。在你的描述变量定义下面添加以下内容。
override var debugDescription: String {
return "debugDescription: " + super.debugDescription
}
建立并运行该应用程序。当调试器停止在断点时,再次打印self。
(lldb) po self
LLDB控制台的输出将类似于以下内容。
debugDescription: Yay! debugging <Signals.MasterViewController: 0x7fb71fd04080>
请注意,由于你实现了debugDescription,现在po self和print命令输出的self有什么不同。当你从LLDB打印一个对象时,被调用的是debugDescription,而不是description。很好!
正如你所看到的,在处理一个
NSObject类或子类时,拥有描述或调试描述将影响po的输出。
那么,哪些对象覆盖了这些描述方法?你可以使用图像查找命令和一个智能的regex查询,很容易找到哪些对象覆盖了这些方法。你在前几章学到的知识已经派上用场了!
例如,如果你想知道所有覆盖debugDescription的Objective-C类,你可以简单地通过输入来查询所有方法。
(lldb) image lookup -rn '\ debugDescription\]'
根据输出结果,Foundation框架的作者似乎在很多基础类型(即NSArray)中加入了debugDescription,以使我们的调试生活更容易。此外,它们也是私有类,也有重载的debugDescription方法。
你可能注意到列表中的一个是CALayer。让我们来看看CALayer中描述和debugDescription的区别。
在你的LLDB控制台,输入以下内容。
(lldb) po self.view!.layer.description
你会看到类似于下面的内容。
"<CALayer: 0x600002e9eb00>"
这有点无聊。现在输入以下内容。
(lldb) po self.view!.layer
你会看到与下面类似的东西。
这就更有趣了--也更有用了! 显然,Core Animation的开发者决定普通的描述应该只是对象的引用,但如果你在调试器中,你会想看到更多的信息。目前还不清楚他们这样做的具体原因。可能是调试描述中的一些信息计算起来很昂贵,所以他们只想在绝对必要的时候才做。
接下来,当你还停在调试器中时(如果没有,就回到viewDidLoad()的断点),对self执行p命令,像这样。
(lldb) p self
你会得到类似于下面的结果。
这可能看起来很吓人,但让我们把它分解一下。
首先,LLDB吐出自己的类名。在这个例子中,是Signals.MasterViewController。
接下来是一个引用,你可以在你的LLDB会话中用来引用这个对象。在上面的例子中,它是$R2。你的会有所不同,因为这是一个LLDB在你使用LLDB时增加的数字。
如果你想在以后的会话中回到这个对象,这个引用是很有用的,也许当你在一个不同的范围中,self不再是同一个对象时。在这种情况下,你可以把这个对象称为$R2。要知道如何做,请键入以下内容。
(lldb) p $R2
你会看到同样的信息再次打印出来。你将在本章后面学习更多关于这些LLDB变量的知识。
在LLDB变量名称后面是这个对象的地址,后面是一些专门针对这种类型的输出。在这个例子中,它显示了与UITableViewController相关的细节,它是MasterViewController的超类,后面是detailViewController实例变量。
正如你所看到的,p命令的输出肉体与po命令不同。p的输出取决于类型格式化:LLDB作者为Objective-C、Swift和其他语言中的每一个(值得注意的)数据结构都添加了内部数据结构。值得注意的是,Swift的格式化在每个Xcode版本中都在积极开发,所以MasterViewController的p的输出对你来说可能是不同的。
由于这些类型的格式化是由LLDB持有的,如果你愿意,你有权力改变它们。在你的LLDB会话中,输入以下内容。
(lldb) type summary add Signals.MasterViewController --summary-string "Wahoo!"
现在你已经告诉LLDB你只想在打印MasterViewController类的实例时返回静态字符串 "Wahoo!"。Signals前缀对于Swift类来说是必不可少的,因为Swift在类名中包含了模块,以防止命名空间的冲突。现在试着打印出self,像这样。
(lldb) p self
输出结果应该与下面类似。
(lldb) (Signals.MasterViewController) $R3 = 0x00007fb71fd04080 Wahoo!
这个格式化将被LLDB在不同的应用启动中记住,所以当你玩完p命令后,一定要删除它。从你的LLDB会话中删除你的,像这样。
(lldb) type summary clear
输入p self现在将回到LLDB格式化作者创建的默认实现。
值得注意的是,在调试你的程序时有两种调试上下文:非Swift调试上下文和Swift上下文。默认情况下,当你在Objective-C代码中停止时,LLDB将使用非Swift(Objective-C)的调试上下文,而如果你在Swift代码中停止,LLDB将使用Swift的上下文。听起来很合乎逻辑,对吗?
如果你突然停止调试器(例如,如果你在Xcode中按下进程暂停按钮),LLDB将默认选择Objective-C上下文。
确保你在上一节中创建的GUI Swift断点仍处于启用状态,并构建和运行该应用程序。当断点出现时,在你的LLDB会话中键入以下内容。
(lldb) po [UIApplication sharedApplication]
LLDB会向你抛出一个古怪的错误。
你已经在Swift代码中停下来了,所以你在Swift上下文中。但你试图执行Objective-C代码。这是不可能的。同样地,在Objective-C上下文中,对一个Swift对象做一个po也不会成功。
你可以用-l选项强制表达式在Objective-C上下文中使用,以选择语言。然而,由于po表达式被映射到表达式-O--,你将无法使用po命令,因为你提供的参数在---之后,这意味着你必须输入表达式。在LLDB中,键入以下内容。
(lldb) expression -l objc -O -- [UIApplication sharedApplication]
这里你已经告诉LLDB使用objc语言的Objective-C。如果有必要,你也可以使用objc+ +来表示Objective-C++。
LLDB将吐出对共享应用程序的引用。尝试在Swift中做同样的事情。因为你已经停在了Swift上下文中,试着用Swift语法来打印UIApplication引用,像这样。
(lldb) po UIApplication.shared
你会得到和你在Objective-C上下文中打印的一样的输出。恢复程序,键入continue,然后突然暂停Signals应用程序。
在那里,按向上的箭头,调出你刚才执行的Swift命令,看看会发生什么。
(lldb) po UIApplication.shared
再一次,LLDB会很烦躁。
error: property 'shared' not found on object of type 'UIApplication' (错误:在'UIApplication'类型的对象上没有找到属性'shared')。
记住,突然停止将使LLDB进入Objective-C上下文。这就是为什么你在试图执行Swift代码时得到这个错误。
你应该时刻注意你目前在调试器中暂停的语言。
正如你之前看到的,LLDB在打印对象时将自动代表你创建局部变量。您也可以创建您自己的变量。
移除程序中的所有断点,构建并运行应用程序。突然停止调试器,使其默认为Objective-C上下文。从那里输入。
(lldb) po id test = [NSObject new]
LLDB将执行这段代码,它创建了一个新的NSObject,并将其存储到test变量中。现在,在控制台中打印测试变量。
(lldb) po test
你会得到一个类似下面的错误。
error: use of undeclared identifier 'test'
这是因为你需要在你希望LLDB记住的变量前加上$字符。
再次声明test,前面加$。
(lldb) po id $test = [NSObject new]
(lldb) po $test
<NSObject: 0x60000001d190>
这个变量是在Objective-C对象中创建的。但是如果你试图从Swift上下文中访问这个变量会发生什么?试试吧,输入以下内容。
(lldb) expression -l swift -O -- $test
到目前为止还不错。现在试着在这个Objective-C类上执行一个Swift风格的方法。
(lldb) expression -l swift -O -- $test.description
你会得到一个这样的错误。
如果你在Objective-C上下文中创建了一个LLDB变量,然后移到Swift上下文中,不要指望一切都 "正常"。这是一个正在积极开发的领域,通过LLDB在Objective-C和Swift之间的衔接可能会随着时间的推移而得到改进。
那么,在LLDB中创建引用如何在实际生活中使用呢?你可以抓取一个对象的引用并执行(以及调试!)你选择的任意方法。为了看到这一点,在MasterViewController的父级视图控制器MasterContainerViewController上创建一个符号断点,使用Xcode的符号断点为MasterContainerViewController的viewDidLoad。
在符号部分,输入以下内容。
Signals.MasterContainerViewController.viewDidLoad() -> ()
请注意参数和参数返回类型的空格,否则断点将无法工作。
你的断点应该是下面的样子。
构建并运行该应用程序。Xcode现在会在
MasterContainerViewController.viewDidLoad()。从那里,输入以下内容。
(lldb) p self
由于这是你在Swift调试环境中执行的第一个参数,LLDB将创建变量$R0。通过在LLDB中输入continue来恢复程序的执行。
现在你没有通过使用self来引用MasterContainerViewController的实例了,因为执行已经离开了viewDidLoad(),转到了
更大、更好的运行循环事件。
哦,等等,你还有$R0这个变量!你现在可以引用MasterContainerController的实例。你现在可以引用MasterContainerViewController,甚至执行任意的方法来帮助调试你的代码。
在调试器中手动暂停应用程序,然后输入以下内容。
(lldb) po $R0.title
不幸的是,你会得到。
error: use of undeclared identifier '$R0'
你突然停止了调试器的运行! 记住,LLDB将默认为Objective-C;你需要使用-l选项来保持在Swift环境中。
(lldb) expression -l swift -- $R0.title
输出将类似于下面的内容,你可能会有一个不同的R编号。
(String?) $R1 = "Quarterback"
当然,这是视图控制器的标题,显示在导航栏中。
现在,输入以下内容。
(lldb) expression -l swift -- $R0.title = "💩 💩 💩 💩 💩 "
通过在Xcode中输入continue或按下播放按钮来恢复应用程序。
注意:要在你的macOS机器上快速访问大便的表情符号,按住⌘ + ⌃ + 空格。从那里,你可以通过搜索 "大便 "这个短语来轻松地找到正确的表情符号。
这是生活中的小事,你要珍惜!
正如你所看到的,你可以很容易地按照你的意愿操纵变量。
此外,你还可以在代码上创建一个断点,执行代码,并使断点被击中。如果你在调试过程中,想通过某个函数的某些输入来观察它的运行情况,这就很有用。
例如,你在viewDidLoad()中仍有一个符号断点,所以试着执行该方法来检查代码。暂停程序的执行,然后输入。
(lldb) expression -l swift -O -- $R0.viewDidLoad()
什么也没发生。断点没有命中。什么原因呢?事实上,MasterContainerViewController确实执行了这个方法,但在默认情况下,LLDB在执行命令时将忽略任何断点。你可以用-i选项禁用这个选项。
在你的LLDB会话中键入以下内容。
(lldb) expression -l swift -O -i 0 -- $R0.viewDidLoad()
LLDB现在会在你之前创建的viewDidLoad()符号断点上断掉。这种战术是测试方法逻辑的一个好方法。例如,你可以实现测试驱动的调试,通过给一个函数不同的参数,看它如何处理不同的输入。
LLDB有一个很好的选项,就是能够对基本数据类型的输出进行格式化。这使得LLDB成为学习编译器如何格式化基本C类型的一个伟大工具。当你探索汇编部分时,这是必须知道的,你将在本书的后面进行探索。
首先,删除之前的符号断点。接下来,构建并运行应用程序,最后突然暂停调试器,以确保你处于Objective-C上下文中。
在你的LLDB会话中键入以下内容。
(lldb) expression -G x -- 10
这个-G选项告诉LLDB你希望输出的格式是什么。G代表的是GDB格式。如果你不知道,GDB是LLDB之前的调试器。因此,这是说无论你指定什么,都是GDB格式的规格。在这个例子中,x被用来表示十六进制。
你会看到下面的输出。
(int) $0 = 0x0000000a
这是十进制的10,打印成十六进制的。哇!
但是等等! 还有呢! LLDB允许你使用一个整洁的速记语法来格式化类型。输入以下内容。
(lldb) p/x 10
你会看到和以前一样的输出。但这样一来,打字就少了很多!
这对于学习C语言数据类型背后的表示法是很好的。例如,整数10的二进制表示法是什么?
(lldb) p/t 10
/t指定了二进制格式。你会看到十进制的10在二进制中是什么样子。当你处理一个比特字段时,这可能特别有用,例如,你可以仔细检查一个给定的数字将被设置哪些字段。
负10的情况如何?
(lldb) p/t -10
小数10的二进制补码。很好!
10.0的浮动点二进制表示呢?
(lldb) p/t 10.0
这可能会派上用场!
字符 "D "的ASCII值如何?
(lldb) p/d 'D'
啊,所以'D'是68! /d规定了十进制格式。
最后,隐藏在这个整数后面的首字母缩写是什么?
(lldb) p/c 1430672467
/c表示的是char格式。它采用二进制数字,分成8位(1字节)的小块,并将每个小块转换为ASCII字符。在这种情况下,它是一个4字符代码(FourCC),表示STFU。嘿!现在好点了吗?
输出格式的完整列表如下(摘自https://sourceware.org/gdb/onlinedocs/gdb/Output-Formats.html )。
x:十六进制
d:十进制
u:无符号十进制
o:八进制
t:二进制
a:地址
c:字符常数
f: float
s:字符串
如果这些格式对你来说还不够,你可以使用LLDB的额外格式化器,尽管你将无法使用GDB的格式化语法。
LLDB的格式器可以这样使用。
(lldb) expression -f Y -- 1430672467
这给你提供了以下输出。
(int) $0 = 53 54 46 55 STFU
这就解释了前面的FourCC代码!
LLDB有以下格式化器(取自http://lldb.llvm.org/varformats.html )。
B:布尔值
b:二进制
y: 字节
Y:带ASCII码的字节
c:字符
C:可打印的字符
F:复数
s:C-字符串
i:十进制
E:枚举
x:十六进制
f:复数
o:八进制
O:OSType
U:unicode16
u:无符号十进制
p:指针
拍拍自己的肩膀--这又是一轮关于你能用表达式命令做什么的大讨论。试着通过执行 "帮助表达式 "来探索其他一些表达式选项,看看你是否能猜出它们的作用。
上一章 | 目录 | 下一章 |
---|