现在你已经学习了如何创建断点, 因此调试器会在你的代码里停下来, 现在是时候从你调试的程序里获取一些有用的信息了.
你应该会经常想要查看对象的实例变量. 但是, 你知道吗你甚至可以通过LLDB执行任意代码?详细说就是通过Objective-C的运行时你可以声明,初始化,并且注入代码来帮助你理解应用程序.
在本章中你将会学习到expression
命令.这条命令允许你在调试器中执行任意代码.
格式化p 和 po
你可能很熟悉go-to
这个调试命令.po
这个命令经常用来打印出对象的信息.可以是一个对象的实例变量, 也可以是一个对象的引用, 还可以是一个寄存器里的对象.它甚至可以是内存里任意对象的内存地址.
如果你在LLDB控制台中查看po
的快速帮助, 你会发现po
实际是一个表达式expression -O --
的缩写. -o
参数用来打印出对象的description
.po
的另一个兄弟指令p
是省略掉-o
选项的表达式的缩写expression --
.
p
打印出的信息取决于LLDB type system
.LLDB的值的格式化类型决定了它的输出并且是完全可以自定义的(后面你就会看到).
是时候学习一下如何用p
和po
获取他们的内容了.在本章中你依然后使用Signals
项目.
在Xcode中打开Signals
项目.接下来打开MasterViewController.swift
并且在这个类的上方加上下面的代码:
override var description: String {
return "Yay! debugging " + super.description
}
在viewDidLoad
中的super.viewDidLoad()
下面加上下面的代码:
print("\(self)")
现在在MasterViewController.swift中在你刚刚添加的打印方法的下面创建一个断点.
构建并运行APP:
当
Signals
在viewDidLoad()
中停下来的时候, 在LLDB控制台中输入下面的代码:
(lldb) po self
你应该会看到下面这些输出:
Yay! debugging <Signals.MasterViewController: 0x7f8a0ac06b70>
注意一下print语句的输出和它与你在调试器中执行po self
输出的匹配度.
你也可以更进一步. NSObject
有另外一个用来调试的description
方法叫debugDescription
.现在来尝试实现一下. 在description
变量定义的下面添加以下代码:
override var debugDescription: String {
return "debugDescription: " + super.debugDescription
}
构建并运行应用程序.当调试器在断点处停下来的时候, 再次打印self
:
(lldb) po self
LLDB控制台的输出看起来应该是下面的样子:
debugDescription: Yay! debugging <Signals.MasterViewController:
0x7fb71fd04080>
注意看po self
和print self
在你实现了debugDescription
之后的输出有什么不同. 当你在LLDB中打印一个对象的时候调用的是debugDescription
而不是description
, 注意到了吗!
正如你看到了, 当NSObject
类或者它的子类有一个description
或者debugDescription
方法的时候会影响到po的输出.
那么哪些对象需要重写description
方法呢?你可以简单的通过image lookup
命令加一个正则表达式捕获那些重写了此方法的对象.你在前面章节中学到的内容将要派上用场了.
例如, 如果你想要知道哪些Objective-C类重写了debugDescription
方法, 你可以通过下面的命令查询所有这些方法:
(lldb) image lookup -rn '\ debugDescription\]'
根据输出的内容可以看到, Foundation
框架的作者已经在许多foundation
类型(例如:NSArray)里面添加了debugDescription
, 让我们在调试的时候更简单. 此外还有一些私有的类也重写了debugDescription
方法.
你可以能会注意到在列表里有CALayer
类. 让我们看一下在CALayer
类中description
和debugDescription
有哪些不同.
在LLDB控制台中输入下面的内容:
(lldb) po self.view!.layer.description
你将会看到类似下面的输出:
"<CALayer: 0x61000022e980>"
只有一点点的信息. 现在输入下面的内容:
(lldb) po self.view!.layer
你将会看到下面这些输出:
<CALayer:0x61000022e980; position = CGPoint (187.5 333.5); bounds =
CGRect (0 0; 375 667); delegate = <UITableView: 0x7fdd04857c00; frame =
(0 0; 375 667); clipsToBounds = YES; autoresize = W+H; gestureRecognizers
= <NSArray: 0x610000048220>; layer = <CALayer: 0x61000022e980>;
contentOffset: {0, 0}; contentSize: {375, 0}>; sublayers = (<CALayer:
0x61000022d480>, <CALayer: 0x61000022da60>, <CALayer: 0x61000022d8c0>);
masksToBounds = YES; allowsGroupOpacity = YES; backgroundColor = <CGColor
0x6100000a64e0> [<CGColorSpace 0x61800002c580> (kCGColorSpaceICCBased;
kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 1 1 1 )>
这里有更多的能容-而且更加有用!很显然Core Animation
的开发者需要通过引用用description
获取更多更清楚的信息. 但是如果你在调试器中, 你将会看到更多信息.我们并不清楚他们为什么要制造这些不同.可能这些debug description
需要执行大量的计算, 因此他们只在绝对必要的时候才用到.
接下来, 你应该还停留在调试器中, 尝试执行p self
:
(lldb) p self
你应该会得到一些类似下面的信息:
(Signals.MasterViewController) $R2 = 0x00007fb71fd04080 {
UIKit.UITableViewController = {
baseUIViewController@0 = <extracting data from value failed>
_tableViewStyle = 0
_keyboardSupport = nil
_staticDataSource = nil
_filteredDataSource = 0x000061800024bd90
_filteredDataType = 0
}
detailViewController = nil
}
这看起来可能有点吓人, 但是让我们来解析一下.
首先, LLDB输出了self的类名. 在这里就是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发行版中可能有少许的不同.
鉴于类型格式化是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!
这个格式在通过APP启动的时候会被LLDB记住, 因此要确保你练习完p
命令之后删除它. 可以用下面的指令在LLDB会话中删除:
(lldb) type summary clear
输入p self
将会回到LLDB作者默认的实现方式.类型格式化是一个值得我们在后面章节中详细讨论的话题.因为它可以在你没有源代码的情况下帮助你详细的调试应用程序.
Swift vs Objective-C调试环境
注意到这里有两个调试环境在调试你的代码的时候是非常重要的:一个非Swift调试环境和一个swift调试环境.默认情况下, 当你在 Objective-C代码中停下来的时候, LLDB将会使用非Swift(Objective-C)调试环境, 当你在Swift代码中停下来的时候, LLDB将会使用Swift调试环境.听起来很合逻辑, 对吧?
如果你将调试器停在了蓝色断点的外面, LLDB默认情况下将会选择Objective-C环境.确保你之前在GUI里面创建的断点依然存在并仍然可用然后构建并运行APP. 当断点触发的时候, 在你的LLDB会话里输入下面的内容:
(lldb) po [UIApplication sharedApplication]
LLDB将会抛出一个错误给你:
error: <EXPR>:3:16: error: expected ',' separator
[UIApplication sharedApplication]
^ ,
你已经在Swift代码中停下来了, 所以你在swift环境中.但是你却尝试运行Objective-C的代码.那是行不通的.类似的在Objective-C环境中运行Swift代码也是行不通的.
你可以用-l
选项选择一个语言强制让表达式使用 Objective-C环境.然而, 由于po
是expression - O --
的缩写, 你将因为提供在--
后面的参数而不能够使用po
命令, 这就意味着你将不得不输入expression
. 在LLDB中, 输入下面的内容:
(lldb) expression -l objc -O -- [UIApplication sharedApplication]
这里你已经告诉LLDB为Objective-C使用objc
语言.如果必要的话你还可以为 Objective-C++用objc++
.
LLDB将会输出shared application的引用.尝试在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'
记住, 在蓝色断点外面停下会让LLDB进入Objective-C环境. 这就是为什么在你尝试执行Swift代码的时候会抛出一个错误.
你因该时刻注意调试器当前停在什么样的语言环境里.
用户定义的变量
正如你之前看到的, LLDB在打印出对象的时候会自动维护局部变量.你同样也可以创建自己的变量.
从程序里移除所有的断点构建并运行APP.在蓝色断点外面停止调试器所以默认的是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对象.但是如果你想在swfit环境中访问这个变量会发生什么呢?尝试输入下面的内容:
(lldb) expression -l swift -O -- $test
到现在为止,一直都还不错.现在尝试在这个Objective-C类上执行swift风格的代码.
(lldb) exppression -l swift -O -- $test.description
你将会得到一个类似下面的错误:
error: <EXPR>:3:1: error: use of unresolved identifier '$test'
$test.description
^~~~~
如果你在Objective-C环境中创建了一个LLDB变量, 然后转到了swift环境中, 不要期望一切都会照常工作.随着时间的流失我们会看到swift和Objective-C通过LLDB桥接的改善.
那么如何在LLDB中创建应用与真实环境的引用?你可以将引用存在一个对象里并执行你选择的任意代码. 要看实际效果, 可以在MasterViewController的父视图控制器里创建一个符号性的断点, MasterContainerViewController
使用了一个Xcode的符号断点在viewDidLoad方法里.
在Symbol部分输入:
Signals.MasterContainerViewController.viewDidLoad () -> ()
要注意参数和返回值之间的空格, 否则断点是不生效的.
你的断点看起来应该是下面这个样子:
构建并运行APP.Xcode现在将会断点在MasterContainerViewController.viewDidLoad().From there, type the following:
(lldb) p self
鉴于这是你再swift调试环境执行的第一个参数, LLDB将会创建一个变量$R0
.在LLDB中输入continue
继续执行程序.
因为执行移到更大和更好的运行循环事件并停留在了viewDidLoad()
里, 所以所以现在你还不能通过使用self
来引用 MasterContainerViewController实例.
但是你仍然有$R0
这个变量!现在你可以应用MasterContainerViewController
甚至可以执行任意代码来帮助你调试.
手动的将APP暂停在调试器中, 然后键入下面内容:
(lldb) po $R0.title
不幸的是, 你将会得到:
error: use of undeclared identifier '$R0'
你将调试器停在了蓝色断点的外面!记住, LLDB默认的是Objective-C环境. 你需要使用-l
选项进入swift环境:
(lldb) expression -l swift -- $R0.title
这将会输出下面的内容:
(String?) $R1 = "Quarterback"
当然, 这是显示在导航栏上的视图控制器的标题.
现在, 输入下面的内容:
(lldb) expression -l swift -- $R0.title = "! ! ! ! ! "
输入continue
继续运行APP.
正如你看到的你可以轻松的操控你想操控的变量.
此外, 你也可以在代码里创建一个断点, 执行代码, 并且在断点触发的时候暂停. 如果你正在调试一些事情并且想用特定的输入执行一个函数看看它是如何执行的时候这是很有用的.
例如, 你仍然有在
viewDidLoad()
里的符号断点, 所以尝试执行那个方法去检查代码. 暂停程序的执行, 然后输入:
(lldb) expression -l swift -O -- $R0.viewDidLoad()
什么事都没有发生. 没有触发断点. 怎么会这样?事实上, MasterContainerViewController
已经执行了这个方法, 但是在默认情况下, LLDB在执行命令的时候会忽略任何断点.你可以用-i
选项经用这个功能.
在LLDB控制台中输入下面的内容:
(lldb) expression -l swift -O -i 0 -- $R0.viewDidLoad()
现在LLDB会在你之前创建的符号断点处停下来.这种策略是测试方法逻辑的绝佳方法.例如, 你可以实现测试驱动的调试, 通过给一个函数不同的参数来看看它如何处理不同的输入.
类型格式化
LLDB中一个好的选项是你可以执行基本数据类型的输出格式. 这让LLDB成为了一个学习编译器是格式化基本的C类型的伟大工具.这在你学习后面的汇编章节的时候是必须知道的.
在LLDB控制台中输入下面的命令:
(lldb) expression -G x -- 10
-G
选项告诉LLDB你想要输出什么样的格式. G代表着GDB格式. 你可能不知道, GDB是LLDB前一代的调试器. 在这里, 使用的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.够清楚吧!
浮点数10.0又如何用二进制表示呢?
(lldb) p/t 10.0
这可能派上用场!
字符D
的ASCII值是怎样的呢?
(lldb) p/d 'D'
所以D
是68!/d
指定的是十进制形式.
最后, 整数后面隐藏的缩写是什么?
(lldb) p/c 1430672467
/c
指明了字符形式. 它将数字转换成二进制, 每8位作为一个整体(1字节), 然后将每一个字节都转换成ASCII字符.在这里, 它有4个字符代码STFU.
下面是所有输出格式的列表(可以参考:[https://sourceware.org/gdb/ onlinedocs/gdb/Output-Formats.html](https://sourceware.org/gdb/ onlinedocs/gdb/Output-Formats.html)):
• x: hexadecimal
• d: decimal
• u: unsigned decimal
• o: octal
• t: binary
• a: address
• c: character constant
• f: float
• s: string
如果这个格式对你来说不够用, 你可以使用LLDB拓展的格式, 但是你将不能够使用GDB格式语法.
LLDB的格式可以像下面这样使用:
(lldb) expression -f Y -- 1430672467
这将会输出下面的内容:
(int) $0 = 53 54 46 55 STFU
这解释了之前的FourCC代码!
LLDB拥有下面的格式(可以参考[http://lldb.llvm.org/
varformats.html](http://lldb.llvm.org/
varformats.html)):
• B: boolean
• b: binary
• y: bytes
• Y: bytes with ASCII
• c: character
• C: printable character
• F: complex float
• s: c-string
• i: decimal
• E: enumeration
• x: hex
• f: float
• o: octal
• O: OSType
• U: unicode16
• u: unsigned decimal
• p: pointer
我们为什么要学习这些?
尝试通过执行help expression
查看其他的expression
选项并查看你可以用它们来做什么.