前言
记录一下比较常用的一些 LLDB 调试技巧.
Note: 在这里是记录一下常用的方法, 并不是完全教程哟!
Note: 前方多图(前方高能), 流量慎入. 土豪无视.
BreakPoint
工作中使用断点对程序进行调试可以说就跟家常便饭一样, 我们几乎天天都会用到. 来看看 Xcode 中如何简单的使用断点吧.
在 Xcode 中设置断点的方法有如下三种:
- 在代码左侧的行数那一列中点击一下, 就会出现一个断点.
- 用鼠标选中你希望下断点的一行, 然后按
Command + \
来设置断点. - 使用
LLDB
指令生成断点.
下图中我们看到的蓝色的矩形, 就是表示该行设置了断点.
如果希望一个断点暂时失效, 点击蓝色矩形区域, 此时蓝色将会变为灰色, 表示断点失效, 如下图:
删除一个断点也很简单, 用鼠标拖住矩形区域, 在代码区域放手就可以了, 此时你会看到一个动效并且会听到噗
的一声. 如下图红色矩形内的动效:
对一个断点进行编辑, 只需要鼠标右键点击断点, 然后选择Edit Breakpoint
, 如下图:
进入断点编辑模式后, 我们将会看到如下的对话框:
在这里我们可以对断点进行编辑.
Condition
: 条件, 这里可以设置断点的出发条件, 例如我们在程序中有一个变量名为 index
, 在这里我们设置条件 index == 1000
, 代表只有当 index
变量为 1000的时候, 断点才会被触发.
Ignore
: 忽略, 在这里可以设置断点被忽略多少次以后触发.
Action
: 我们可以为断点触发的时候添加事件, 譬如: 语音啊, 音效啊, LLDB
指令之类的. 我们会在实战环节使用 Action
来看看效果哈, 不要急.
Options
: 把这个对勾勾上的话, 程序不会终止在断点的位置, 而是会继续运行.
来看看下面这个例子:
- (void) addLabel {
UILabel *label = [[UILabel alloc] init];
label.text = @"Test LLDB";
[self.view addSubview: label];
}
- (void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self addRedView];
}
我们写了一个方法 addLabel
, 用来添加一个 label
对象, 每次点击 self.view
都会触发 addLabel
方法, 可以看到代码中我们并没有设置 label
对象的 frame
, 所以无论我们如何点击屏幕, label
都是无法显示出来的, 我们现在为 [self.view addSubview: label]
这句代码所在的行下一个断点, 然后编辑这个断点, 如下图所示:
我来解释一下这个断点的意义:
- 首先是条件, 只有在
label.text
为@"Test LLDB"
的时候, 断点才会被触发. - 其次是忽略次数, 该断点会被忽略两次, 也就是你前两次点击屏幕的时候, 该断点是被忽略掉的.
- 接下来是事件, 在这个断点中, 我添加了三个事件,
Sound
事件: 触发断点会有一个提示音效.Shell Command
事件: 我这里设置的是say
, 就是将下面那句话读出来,%H
代表断点被Hit
的次数.Debugger Command
事件: 可以添加LLDB
指令, 在这里我们执行了一句代码, 给label
对象的frame
属性进行了赋值.Log Message
事件: 在这个事件中, 你可以选择将你输入的文字打印到控制台中 或 读出来.%H
代表断点被Hit
的次数,%B
代表函数名. - 最后自动继续运行程序.
除了普通的断点, Xcode 还提供其他类型的断点, 例如 Exception Breakpoint
, 我们调试程序的过程中, 我相信很多小伙伴都遇到过那种非常头疼的 Crash
, 就是程序直接Crash
到了 main
函数中, 这种问题相当的不好定位, 此时可以添加一个 Exception Breakpoint
断点来捕获异常.
![设置 Exception Breakpoint]
](http://upload-images.jianshu.io/upload_images/2452150-1e05b780abe42e61.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
在 Xcode 中使用 Command + 7
来查看当前程序中的所有断点. 在这里我们也可以管理断点, 例如删除断点, 关闭断点, 编辑断点等操作. 如下图:
来看看下面这张图, 我来依次介绍下图中按钮的功能:
按钮从左至右:
- 收起控制台
- 开启/关闭 所有断点: 蓝色代表开启, 灰色代表关闭
- 暂停/继续 程序: 该按钮默认行为是暂停应用程序, 如果程序当前处于暂停状态, 那么点击该按钮为允许程序正常执行下去(一直执行下去, 或遇到下一个断点).
- 下一步: 会以黑盒的方式执行一行代码。如果所在这行代码是一个函数调用,那么就不会跳进这个函数,而是会执行这个函数,然后继续。
- 进入函数
- 退出函数
这几个按钮的作用, 大家在程序中下个断点自己点点试试就知道了, 很简单的. 这就是 Xcode 中断点的最最最基本的应用.
LLDB
介绍
语法
po
& p
-
po
: 打印一个Objective-C
对象,po
指令实际上是expression -O --
指令的别名. -
p
: 打印类似int
、float
等基本数据类型和类似CGRect
、CGPoint
等结构体. (p
是print
的缩写, 你还可以使用print
、prin
、pri
)
看下面这个代码块, 在 -(void) viewDidLoad
方法中声明了几个变量, 我们用 po
和 p
来打印一下看看效果.
// ViewController.h
@interface ViewController ()
@property (nonatomic, strong) UILabel *titleLabel;
@end
// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 声明两个结构体
CGRect rect = CGRectMake(0, 0, 100, 100);
CGPoint point = CGPointMake(100, 100);
// 2. 声明两个基本数据类型
NSInteger index = 100;
CGFloat width = 200.0f;
// 3. 声明两个 Objective-C 对象
NSArray *array = [NSArray arrayWithObjects: self.titleLabel, @"LLDBDemo", nil];
NSDictionary *dictionary = @{
@"kMLObject" : self.titleLabel,
@"kMLTitle" : @"LLDBDemo",
};
// 4. 创建 titleLabel
self.titleLabel = [UILabel new];
self.titleLabel.text = @"LLDBDemo";
self.titleLabel.font = [UIFont systemFontOfSize: 16];
self.titleLabel.textColor = [UIColor blackColor];
[self.view addSubview: self.titleLabel];
// 在这里打一个断点
}
@end
ok, 代码片段看完了, Command+R
运行程序, 当程序运行到断点终止时, 我们可以再控制台使用 po
或 p
命令来打印我们刚才声明的变量. 效果如下:
(lldb) p rect
(CGRect) $0 = (origin = (x = 0, y = 0), size = (width = 100, height = 100))
(lldb) p point
(CGPoint) $1 = (x = 100, y = 100)
(lldb) p index
(NSInteger) $2 = 100
(lldb) p width
(CGFloat) $3 = 200
(lldb) po array
<__NSArrayI 0x610000229580>(
<UILabel: 0x7fb5a2d0b6a0; frame = (0 0; 0 0); text = 'LLDBDemo'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60800008c760>>,
LLDBDemo
)
(lldb) po dictionary
{
kMLObject = "<UILabel: 0x7fb5a2d0b6a0; frame = (0 0; 0 0); text = 'LLDBDemo'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60800008c760>>";
kMLTitle = LLDBDemo;
}
(lldb) po self.titleLabel
<UILabel: 0x7fb5a2d0b6a0; frame = (0 0; 0 0); text = 'LLDBDemo'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x60800008c760>>
除了以上你看到的这些打印, 你还可以打印更细节的东西, 以上面代码块中的 titleLabel
和 array
为例:
(lldb) po self.titleLabel.frame
(origin = (x = 0, y = 0), size = (width = 0, height = 0))
(lldb) po self.titleLabel.frame.size.width
0
(lldb) po self.titleLabel.text
LLDBDemo
(lldb) po [array objectAtIndex: 1]
LLDBDemo
可以看到 po
和 p
的功能已经很强大了是不?
expression
& expr
& e
在我们调试程序的时候, 经常会有需要修改一个变量值得场景. 普通的调试方法, 我们可能会添加一行代码, 然后重新 Command + R
运行程序, 但这必然会无畏的消耗很多时间. 此时使用 expression
指令就非常的方便. 举例来说: 我们创建一个 UIView
实例, 添加到 self.view
中, 代码如下:
UIView *redView = [[UIView alloc] init];
redView.frame = CGRectMake(20, 40, 100, 100);
redView.backgroundColor = [UIColor redColor];
[self.view addSubview: redView];
我们可以在 [self.view addSubview: redView];
这行代码的位置下一个断点, 然后执行如下的两行命令:
e redView.frame = CGRectMake(100, 100, 200, 200)
e redView.backgroundColor = [UIColor blueColor]
然后让程序继续运行起来看看效果, 可以看到原本应该为红色的 view
现在变成了蓝色, 并且 view
原本的位置和大小也发生了改变. 所以expression
指令不仅会改变调试器中的值, 它实际上是真正的改变了程序中的值, 有了这个东西, 代码调试起来可就太爽了. 有些时候你可能不想继续运行程序, 但是仍然想看到你修改的效果, 那怎么办? 此时就应该执行完你的修改之后, 刷新一下界面, 代码如下:
e redView.frame = CGRectMake(100, 100, 200, 200)
e redView.backgroundColor = [UIColor blueColor]
e [CATransaction flush]
刷新页面之后, 你无需继续运行程序, 就可以马上看到效果.
call
call
指令代表着调用某个方法. 实际上call
、 p
、print
这三个指令都是 expression
指令的别名, 实际上的运行效果是一样的, 举例来说明, 看如下代码块:
(lldb) print self.view
(UIView *) $2 = 0x00007f9769509660
(lldb) expression self.view
(UIView *) $3 = 0x00007f9769509660
(lldb) call self.view
(UIView *) $4 = 0x00007f9769509660
(lldb) e self.view
(UIView *) $5 = 0x00007f9769509660
(lldb) p self.view
(UIView *) $6 = 0x00007f9769509660
可以很清楚地看到, 这几个指令实际上的效果是一样的.
$
符号
上文中简介p
的时候, 我们看到代码块中会有这样的东西, 例如: (NSInteger) $2 = 100
、 (CGFloat) $3 = 200
. 这些以$
符号开头的东东实际就是 LLDB
的命名空间的产物, 我们可以用这些东东来进行调试, 来看看下面这段:
// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CGRect rect = CGRectMake(0, 0, 100, 100);
// 在这里打一个断点
}
@end
在 viewDidLoad
中随便声明了一个CGRect
变量, 然后在下方打上一个断点, 我们用如下指令调试一下:
(lldb) p rect
(CGRect) $3 = (origin = (x = 0, y = 0), size = (width = 100, height = 100))
(lldb) e $3 = CGRectMake(10,10,10,10)
(CGRect) $4 = (origin = (x = 10, y = 10), size = (width = 10, height = 10))
(lldb) p rect
(CGRect) $5 = (origin = (x = 10, y = 10), size = (width = 10, height = 10))
(lldb)
可以看到, 当我们 p rect
的时候, 打印的值是我们最初赋的值, 然后我们给 $3
赋了一个CGRectMake(10,10,10,10)
之后, 再来 p rect
, 可以看到此时的 rect
变量已经被我们改变了.
Variable 变量
在某些场景中, 我们调试代码的时候可能需要创建新的变量来辅助我们, 此时我们仍然不需要修改代码后Command+R
重新跑程序, LLDB
同样提供了相应的方法. 我们可以像正常写代码一样, 创建一个 UIView
的实例, 然后将它添加到 self.view
中, 但是唯一不同的是, 声明的变量名需要以美元符号$
开头. 看下面这个代码块:
(lldb) expression UIView *$view = [[UIView alloc] init]
(lldb) expression $view.backgroundColor = [UIColor blackColor]
(lldb) expression $view.frame = CGRectMake(0, 300, 100, 100);
(lldb) expression [self.view addSubview: $view]
此时我们将程序继续运行, self.view
中就会新增了一个黑色的view
.
thread backtrace
& bt
查看调用堆栈
bt
指令就是查看调用堆栈的信息, 调用堆栈信息在程序运行到断点时 或 崩溃时, 在 Xcode 左侧导航区域中会自动显示出来, 如下图:
图中可以清晰的看到, 程序当前正处于 Thread1
的 addRedView
方法中 (这玩意不会看的请自行 Google吧宝贝儿) . 除了在 Xcode 左侧导航区域中我们可以查看调用堆栈, 我们同样还可以使用 LLDB
为我们提供的 bt
指令进行查看. bt
指令只会查看当前线程的调用堆栈, 如果你希望查看全部的调用堆栈, 那么就需要使用 bt all
指令了. (Note: 左侧数字代表了堆栈块的编号, 这个一会我们会用到.)
frame
相关指令
讲解 frame
相关指令之前, 先来看一小段示例代码:
- (void) viewDidLoad {
[super viewDidLoad];
// 1. Create Blue View
UIView *blueView = [[UIView alloc] initWithFrame: CGRectMake(20, 20, 100, 100)];
[blueView setBackgroundColor: [UIColor blueColor]];
[self.view addSubview: blueView];
// 2. Add Red View
[self addRedView];
}
- (void) addRedView {
UIView *redView = [[UIView alloc] initWithFrame: CGRectMake(20, 140, 100, 100)];
[redView setBackgroundColor: [UIColor redColor]];
[self.view addSubview: redView]; // 断点所在行
}
Command+R
运行, 程序将会停止在 [self.view addSubview: redView];
这一行.
frame info
& fr info
查看当前堆栈信息, 以上文提到示例代码为例执行以下命令:
(lldb) frame info
frame #0: 0x000000010a952676 LLDBDemo`-[ViewController addRedView](self=0x00007f8f28409eb0, _cmd="addRedView") + 230 at ViewController.m:88
可以看到, frame info
指令可以查看当前所在堆栈的信息. 其中包括方法名
、文件名
、行号
等信息.
frame select
& fr sel
在工作中, 我们可能会有这样的需求, 在调试一个相对较复杂的程序时, 我们可能会打很多断点, 然后一个断点一个断点的追, 但是有时操作失误错过了某个断点, 我们又要重新来过, 这同样会消耗很多无畏的时间. 以上文的示例代码为例, 此时程序由于断点的原因停在了 [self.view addSubview: redView];
这一行, 并且刚才我们也使用 frame info
指令查看了当前的堆栈信息, 我们当前处在addRedView
方法中, 如果此时我希望修改 viewDidLoad
方法中的 blueView
变量怎么办呢? 我用先 po
一下试试:
(lldb) po blueView
error: use of undeclared identifier 'blueView'
结果显然是不行的, 因为当前堆栈中, 并没有 blueView
, 也就是说如果我们希望对 blueView
进行任何操作, 我们需要做的第一步, 就是切换到 blueView
对应的堆栈当中, 那我们如何切换呢? 还记得bt
指令么? 我们先用 bt
指令查看一下调用堆栈信息, 如下图:
上面这张图我只截取了一部分, 可以看到 *
代表的就是当前堆栈. 还可以看出, viewDidLoad
方法的堆栈编号为 #1
. 此时我们使用指令frame select 1
就能切换到 viewDidLoad
方法所在的堆栈块中:
(lldb) frame select 1
frame #1: 0x0000000108124502 LLDBDemo`-[ViewController viewDidLoad](self=0x00007f8d95905bc0, _cmd="viewDidLoad") + 354 at ViewController.m:81
78 [self.view addSubview: blueView];
79
80 // 2. Add Red View
-> 81 [self addRedView];
82 }
83
84 - (void) addRedView {
我们切换到了 blueView
对应的堆栈中, 就可以对blueView
变量进行想要的操作了. 例如:
(lldb) po blueView
<UIView: 0x7f8d9350b660; frame = (20 20; 100 100); layer = <CALayer: 0x608000028c80>>
Perfect! 完美!
thread return
thread return
指令有一个可选参数, 该参数接收一个表达之, 调用thread return
指令后将会直接跳出当前栈帧, 并且返回表达式的值. 这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。(查看中文原文, 英文原文)
假设我们有一个方法, 是用来判断传入的 MLPerson
对象是否是男生的, 但是现在我们希望该方法, 无论何时都返回 YES
, 此时我们只需要在该方法的最前面下一个断点, 然后执行 thread return YES
就 OK 了, 如下:
- (BOOL) isBoy:(MLPerson *) person {
// 在这里下一个断点, 并且执行 thread return YES 指令.
return person.gender;
}
breakpoint
本文最开始的部分已经介绍过如何使用 Xcode 的 UI 界面来设置断点, 在这部分介绍如何使用 LLDB
来下断点. (breakpoint
这一部分内容出自这里).
breakpoint set -n
根据方法名设置断点, 假如我们希望给所有类中的 addLabel
方法下一个断点:
(lldb) breakpoint set -n addLabel
Breakpoint 2: 4 locations.
breakpoint set -f
针对某一文件中的某一方法设置断点, 如果方法没有写在文件中(例如父类中, Category 中), 那么设置该断点将会失败.
(lldb) breakpoint set -f ViewController.m -n addLabel
Breakpoint 3: where = LLDBDemo`-[ViewController addLabel] + 16 at ViewController.m:93, address = 0x000000010b81f480
breakpoint set -l
针对某一文件中的某一行设置断点.
(lldb) breakpoint set -f ViewController.m -l 101
Breakpoint 5: where = LLDBDemo`-[ViewController touchesBegan:withEvent:] + 96 at ViewController.m:101, address = 0x000000010b81f5a0
breakpoint set -c
设置条件断点(对于条件断点不明确的小伙伴请在本文中第一部分查看).
(lldb) breakpoint set -f ViewController.m -n isNilString: -c string.length
Breakpoint 9: where = LLDBDemo`-[ViewController isNilString:] + 39 at ViewController.m:107, address = 0x000000010b81f637
breakpoint set -o
设置一个单次断点, 该断点只会触发一次:
(lldb) breakpoint set -f ViewController.m -n addLabel -o
Breakpoint 10: where = LLDBDemo`-[ViewController addLabel] + 16 at ViewController.m:93, address = 0x000000010b81f480
breakpoint list
使用该指令查看设置了哪些断点, 如下:
(lldb) br li
Current breakpoints:
19: name = 'addLabel', locations = 1, resolved = 1, hit count = 4
19.1: where = LLDBDemo`-[ViewController addLabel] + 16 at ViewController.m:93, address = 0x000000010b81f480, resolved, hit count = 4
breakpoint disable
& breakpoint enable
我们可以使用这两个指令设置断点是否开启(是否可用), 如下:
// 让断点 19 暂时失效
(lldb) breakpoint disable 19
1 breakpoints disabled.
// 让断点 19 生效
(lldb) breakpoint enable 19
1 breakpoints enabled.
breakpoint delete
该指令代表删除断点, 我们可以删除对应编号的断点, 如下:
(lldb) breakpoint delete 19
1 breakpoints deleted; 0 breakpoint locations disabled.
我们也可以删除所有的断点, 删除所有断点的时候, 我们会得到一个提示, 让我们确认是否删除所有断点, 此时我们输入y
代表确认删除, 如下:
(lldb) breakpoint delete
About to delete all breakpoints, do you want to do that?: [Y/n] y
All breakpoints removed. (1 breakpoint)
如果你觉得这个提示太烦了, 你也可以使用 -f
指令来直接删除所有断点, 如下:
(lldb) breakpoint delete -f
All breakpoints removed. (1 breakpoint)
breakpoint command
在某些特定的时候, 当一个断点被触发了之后, 我们可能需要执行一些指令. 比如每次触发断点, 我们都会打印一下堆栈信息, 此时我们可以为断点需要添加bt
指令, 这样就可以避免每次触发断点后, 我们再手动输入指令了.
breakpoint command add
想为一个断点添加命令, 首先我们必须要创建一个断点, 如下:
(lldb) breakpoint set -f ViewController.m -n addLabel
Breakpoint 19: where = LLDBDemo`-[ViewController addLabel] + 16 at ViewController.m:93, address = 0x000000010b81f480
通过设置断点, 我们可以看到, 当前这个断点的编号为 19. 那么接下来, 我们就为编号为19的断点添加指令, 如下:
(lldb) breakpoint command add -o "bt" 19
此时, 编号为 19 的断点, 就已经增加了一条 bt
指令, 当每次触发该断点的时候, 都会在控制台输出堆栈信息. 在上面代码块中的 -o
指令的完整写法是--one-liner
, 表示增加一条指令. 如果我们需要给该断点增加更多的指令, 此时我们就不要使用 -o
命令了, 应该像如下这么写:
(lldb) breakpoint command add 19
Enter your debugger command(s). Type 'DONE' to end.
> bt
> continue
> DONE
在这里我们为编号为 19 的断点增加了两条指令分别是 bt
和 continue
, 当我们指令输入完毕, 再输入 DONE
就代表结束. Note: 多次对同一个断点添加指令, 后面的指令则会覆盖前面的指令.
breakpoint command list
使用该指令, 可以查看某一断点中附加的指令, 我们尝试查看一下编号为 19 的断点中附加的指令, 如下:
(lldb) breakpoint command list 19
Breakpoint 19:
Breakpoint commands:
bt
continue
breakpoint command delete
使用该指令, 可以删除某一断点中附加的指令, 我们尝试删除一下编号为 19 的断点中附加的指令, 如下:
(lldb) breakpoint command delete 19
(lldb) breakpoint command list 19
Breakpoint 19 does not have an associated command.
breakpoint
这一部分中的命令, 其实完全可以使用 Xcode 为我们提供的 UI 界面来实现, 更直观, 更方便. 所以这部分基本上就是从这篇文章中摘抄过来的. 有兴趣的同学可以看看原文, 写的还是挺 Nice 的.
流程控制
流程控制这个东西, 实际上上文中也有提到过, 还记得这张图么:
如果已经忘了这几个按钮的作用了, 那就翻到本文最初的位置进行查看.
按钮从左往右依次对应的指令为:
-
process continue
&continue
&c
-
thread step-over
&next
&n
-
thread step-in
&step
&s
-
step-out
&finish
常用快捷键
Note: 这部分同样出自这里
功能 | 命令 |
---|---|
暂停/继续 | Command + Ctrl + Y |
断点设置/删除 | Command + \ |
断点失效/生效 | Command + Y |
控制台显示/隐藏 | Command + Shift + Y |
光标切换到控制台 | Command + Shift + C |
清空控制台内容 | Command + K |
实战
说了这么多, 终于到了实战的时候了, 有人说, 为什么一个 LLDB
操作还要实战呢? 原因在于.... 这里面真的好多坑啊, 没有想象中的那么简单.
暂时先写这么多, 未完待续
Lemon龙说:
如果您在文章中看到了错误 或 误导大家的地方, 请您帮我指出, 我会尽快更改
如果您有什么疑问或者不懂的地方, 请留言给我, 我会尽快回复您
如果您觉得本文对您有所帮助, 您的喜欢是对我最大的鼓励
如果您有好的文章, 可以投稿给我, 让更多的 iOS Developer 在简书这个平台能够更快速的成长