第5章:表达

现在你已经学会了如何设置断点,使调试器在你的代码中停下来,现在是时候从你调试的任何软件中获得有用的信息了。

你经常想检查对象的实例变量。但是,你知道你甚至可以通过LLDB执行任意代码吗?更重要的是,通过使用Swift/ObjectiveC的API,你可以在FLY上声明、初始化和注入代码,帮助你理解程序。

在本章中,你将了解到表达式命令。这允许你在调试器中执行任意的代码。

格式化p和po

你可能熟悉常用的调试命令,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与Objective-C的调试语境

值得注意的是,在调试你的程序时有两种调试上下文:非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 )。

如果这些格式对你来说还不够,你可以使用LLDB的额外格式化器,尽管你将无法使用GDB的格式化语法。

LLDB的格式器可以这样使用。

(lldb) expression -f Y -- 1430672467

这给你提供了以下输出。

(int) $0 = 53 54 46 55      STFU

这就解释了前面的FourCC代码!

LLDB有以下格式化器(取自http://lldb.llvm.org/varformats.html )。

接下来去哪儿?

拍拍自己的肩膀--这又是一轮关于你能用表达式命令做什么的大讨论。试着通过执行 "帮助表达式 "来探索其他一些表达式选项,看看你是否能猜出它们的作用。


上一章 目录 下一章