开始学习LLDB命令(第五章:表达式)

现在你已经学习了如何创建断点, 因此调试器会在你的代码里停下来, 现在是时候从你调试的程序里获取一些有用的信息了.
你应该会经常想要查看对象的实例变量. 但是, 你知道吗你甚至可以通过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的值的格式化类型决定了它的输出并且是完全可以自定义的(后面你就会看到).
是时候学习一下如何用ppo获取他们的内容了.在本章中你依然后使用Signals项目.
在Xcode中打开Signals项目.接下来打开MasterViewController.swift并且在这个类的上方加上下面的代码:

override var description: String {
  return "Yay! debugging " + super.description
}

viewDidLoad中的super.viewDidLoad()下面加上下面的代码:

print("\(self)")

现在在MasterViewController.swift中在你刚刚添加的打印方法的下面创建一个断点.
构建并运行APP:

page56image16240.png

SignalsviewDidLoad()中停下来的时候, 在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 selfprint 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类中descriptiondebugDescription有哪些不同.
在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环境.然而, 由于poexpression - 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 () -> ()

要注意参数和返回值之间的空格, 否则断点是不生效的.
你的断点看起来应该是下面这个样子:

page62image17352.png

构建并运行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.

page64image1064.png

正如你看到的你可以轻松的操控你想操控的变量.
此外, 你也可以在代码里创建一个断点, 执行代码, 并且在断点触发的时候暂停. 如果你正在调试一些事情并且想用特定的输入执行一个函数看看它是如何执行的时候这是很有用的.
例如, 你仍然有在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选项并查看你可以用它们来做什么.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容