一、引言
1.1 目的
程序员日常编写代码的时间远小于调试的时间,所以熟练掌握调试工具能够大幅降低调试所要花的时间,从而大幅提高工作效率。本文针对iOS开发过程中通过Xcode与/和LLDB进行调试进行的讲解,以提升对LLDB的掌握。如果同时能够帮助到广大网友,那么我讲十分开心。
1.2 日常调试
在日常工作中,大部分调试动作都是设置一些断点,在程序被暂停时查看执行逻辑、变量的值等,然后定位问题,并根据具体问题修改代码。这应该是大部分程序员的日常调试行为与状态。此种调试行为仅使用LLDB所提供的极少的能力。
除此之外,在大部分情况下程序员会借助在源代码里添加NSLog之类的日志代码,来进行辅助调试。
1.3 日常调试的弊端
- 效率低下
- 比如,在某个循环中,需要在第N次循环才暂停,但是前N-1次也需要暂停与跳过
- 比如,想查看某些UI效果,需要临时修改代码,重新运行
- 比如,调试过程中的修改,需要反复多次,每次都重新编译与运行花费时间较多
- 比如,在一个复杂的UI结构中,不知道某个按钮的点击事件被谁响应
- 比如,某个元素布局错误,不知道谁引起了布局错误,花大力气进行查找
- 会在正常代码之外引入非必要代码
- 添加的NSLog之类代码没有删除
- 添加的其他有作用的调试代码遗漏删除(如,把测试过程中的数据请求地址发布到了证实版本)
二、LLDB命令
LLDB是开源、内置于XCode、具有REPL(read-eval-print-loop)特征的调试器,其提供C++、Python接口。在日常的开发和调试过程中给开发人员带来了非常多的帮助。在Xcode5.0的时候,LLDB全面取代了GDB,使调试工作更加高效、便捷。LLDB是LLVM的一部分。本节重点讲述LLDB的命令,这些命令大部分可以可通过Xcode提的GUI界面来完成,也可以使用Xcode提供的调试控制台以交互命令的方式执行。
2.1 命令概述
LLDB命令的格式格式如下,其中[]表示是可选部分。
<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument[argument...]]
- command
- 基于LLDB运行环境创建执上下文环境,最终的命令在此上下文环境中执行
- subcommand
- 对command创建的上下文环境进行修改,创建更具体的新的上下文环境。子命令以空格分隔,用于组织相关操作。
- action
- 在command、subcomand所形成的上下文环境中需要执行的动作
- options
- 动作所需要的选项、命令可能包含一个或多个选项 (option)
- 选项以空格分隔,以双短划线 (--)开头,组合修改要执行的操作
- 有些option还提供使用单个短划线 (-) 的简写形式。例如,第一次导致程序停止后自动删除breakpoint的breakpoint set --one-shot命令,可以写为breakpoint set -o。
- 有些option还会传递值,option和值用空格分隔。例如,为breakpoint set命令传递--name option,选项值为函数、方法名称,这样就可以为指定函数、方法添加breakpoint。
- 有些命令组合使用options。例如,breakpoint set命令组合使用--file和--line选项,并为其指定对应文件名称和行号。
- Option values和arguments中的空格用双引号保护。如果参数中有双引号,则可以使用反斜杠()标志双引号。LLDB等效的使用单引号和双引号。例如:
(lldb)command [subcommand] -option "some \"quoted\" string"
也可以写为:
(lldb) command [subcommand] -option 'some "quoted" string'
这样的命令解析设计有助于LLDB命令语法在所有命令中保持统一。 - 如果命令即接受options,又接受自由形式 (free form) 的arguments,则必须在最后一个option和第一个argument间放置以空格分隔的双短划线,如expression命令。这样能够确保以短划线开头的argument被解释为argument,而不会因其与option都以短划线开头而被错误解释。
- arguement
- 命令所需要参数
- 命令可能需要一个或多个参数,参数以空格分隔,用于指示要执行的操作。例如,breakpoint disable命令参数传递要禁用的breakpoint。breakpoint disable 1 2表示禁用breakpoint ID为1和2的断点。
2.2.1 命令形式
相同的命令可以用不同的形式实现,例如——以下命令表示同一操作:验证变量someVariable描述,并输出结果。
- 规范形式(canonical form)expression --object-description -- someVariable。
- 缩写形式 (abbreviated form)e -O -- someVariable。
- 别名 (alias)po someVariable。可以为任何命令创建别名,以方便执行常见操作。
2.2.2 命令补全(Command Completion)
LLDB支持源文件名,符号名,文件名,等等的命令补全(Commmand Completion)。终端窗口中的补全是通过在命令行中输入一个制表符来初始化的。Xcode控制台中的补全与在源码编辑器中的补全方式是一样的:补全会在第三个字符被键入时自动弹出,或者通过Esc键手动取消弹出。
命令中的私有选项可以有不同的完成者(completers)。如breakpoint中的--file <path>选项作为源文件的完成者,--shlib <path>选项作为当前加载的库的完成者,等等。这些行为是特定的,例如,如果指定--shlib <path>,且以--file <path>结尾,则LLDB只会列出由--shlib <path>指定的共享类库。
2.2 命令详解
2.2.1 帮助类命令
2.2.1.1 help
help命令可以查看LLDB的帮助文档,方便进一步了解LLDB命令结构。
只输入help命令,将返回所有命令列表,如下是部分命令:
Debugger commands:
apropos -- List debugger commands related to a word or subject.
breakpoint -- Commands for operating on breakpoints (see 'help b' for
shorthand.)
command -- Commands for managing custom LLDB commands.
disassemble -- Disassemble specified instructions in the current
target. Defaults to the current function for the
current thread and stack frame.
expression -- Evaluate an expression on the current thread. Displays
any returned value with LLDB's default formatting.
frame -- Commands for selecting and examing the current thread's
stack frames.
gdb-remote -- Connect to a process via remote GDB server.
If no host is specifed, localhost is assumed.
gdb-remote is an abbreviation for 'process connect
--plugin gdb-remote connect://<hostname>:<port>'
gui -- Switch into the curses based GUI mode.
可以通过help command
来查看命令的具体帮助信息
2.2.1.2 apropos
当我们只记得某个单词,但是不太确定属于某个命令、子命令等时,可以通过此命令进行查找,以获取更加详细的信息。
(lldb) apropos jump
The following commands may relate to 'jump':
_regexp-jump -- Set the program counter to a new address.
thread jump -- Sets the program counter to a new address.
jump -- Set the program counter to a new address.
2.2.2 expression
此命令在当前线程执行表达式并把结果输出,输出结果是默认采用LLDB内置的格式化程序。此命令最重要的用途有两个:被用来输出结果(我们经常使用的是po、p,就是它的简写)、修改变量的值
2.2.2.1 po
po命令在当前线程上执行表达式,并输出表达式的计算结果。
此命令其实是expression -O --
的简写形式,其中-O
实际上是--object-description
,此选项表示使用类型提供的描述来输出结果,对于OC语言来说就是descriprtion。
'po' is an abbreviation for 'expression -O --'
-O ( --object-description )
Display using a language-specific description API, if possible.
其使用非常简单,但是其背后的原理比较复杂。通过下图,我们可以看到其最少要经过两次编译与运行,所花的时间成本还是比较高的。
2.2.2.2 p
p命令在当前线程上执行表达式,并输出表达式的计算结果,此结果以LLDB内置的格式标识。此命令其实是expression --
的简写形式。
'p' is an abbreviation for 'expression --'
其使用非常简单,但是其背后的原理比较复杂。与po命令相比,其第二次是采用动态类型解析、并使用LLDB内置的类型描述输出表达式的结果。
2.2.2.3 v
此命令不属于expression,但是其与po、p作用类似,所以一并在这里介绍。
命令的作用是:显示当前堆栈帧的变量,默认为所有参数和当前范围内的变量。
可以指定聚合变量的子项例如“var->child.x”。 'frame variable' 中的 -> 和 [] 运算符不调用运算符重载(如果存在),但直接访问指定的元素。
'v' is an abbreviation for 'frame variable'
其使用非常简单,但是其背后的原理比较复杂。与po、p相比,其背后是最简单的逻辑。
2.2.2.4 po-VS-p-VS-v
以下是po、p、v的对比
2.2.3 breakpoint
此命令是与用于设置、取消、修改断点等操作。
2.2.3.1 设置断点
使用breakpoint 的子命令 set 设置断点
具体信息可以通过help breakpoint set 查询所有可选的选项(options)以及参数(arguments).
// 按方法名称设置--name(-n):
(lldb) breakpoint set -n greet
Breakpoint 2: where = Greeter`Greeter.Greeter.greet(personNamed: Swift.String) -> () + 127 at Greeter.swift:9:12, address = 0x00000001000034cf
// 按行数设置--line(-l):
(lldb) breakpoint set -l 18
Breakpoint 3: where = Greeter`main + 14 at Greeter.swift:18:15, address = 0x000000010000315e
按文件设置--file(-f):
(lldb) breakpoint set -f Greeter.swift -l 20
Breakpoint 6: where = Greeter`main + 34 at Greeter.swift:20:1, address = 0x0000000100003172
// 根据语言捕获异常--language-exception(-E):
(lldb) breakpoint set -E Objc
Breakpoint 7: no locations (pending).
// 可以通过--exception-typename(-O) 指定要捕获的异常类型:
(lldb) breakpoint set -E Swift -O EnumErrorType
Breakpoint 8: no locations (pending).
// pending,说明某块还没有被加载
2.2.3.2 获取所有断点
此命令获取的是所有的断点,包括通过GUI设置的断点和通过Xcode 所提供的的LLDB交互窗口。
// 获取断点列表: breakpoint list
(lldb) breakpoint list
Current breakpoints:
1: name = 'sayHello', locations = 0 (pending)
2: name = 'greet', locations = 1
2.1: where = Greeter`Greeter.Greeter.greet(personNamed: Swift.String) -> () + 127 at Greeter.swift:9:12, address = Greeter[0x00000001000034cf], unresolved, hit count = 0
3: file = '/Users/jackli/Desktop/Roborock/Greeter.swift', line = 18, exact_match = 0, locations = 1
3.1: where = Greeter`main + 14 at Greeter.swift:18:15, address = Greeter[0x000000010000315e], unresolved, hit count = 0
4: file = 'main.swift', line = 20, exact_match = 0, locations = 0 (pending)
5: file = 'Greet.swift', line = 20, exact_match = 0, locations = 0 (pending)
6: file = 'Greeter.swift', line = 20, exact_match = 0, locations = 1
6.1: where = Greeter`main + 34 at Greeter.swift:20:1, address = Greeter[0x0000000100003172], unresolved, hit count = 0
7: Exception breakpoint (catch: off throw: on) the correct runtime exception handler will be determined when you run
8: Swift Error breakpoint the correct runtime exception handler will be determined when you run
需要注意的是: 设置断点其实是设置了逻辑断点, 可能会对应一个或多个位置断点(location).
每个逻辑断点都有一个按照顺序从1开始分配的整型ID. 每一个位置断点又有一个自己的位置ID, 与逻辑断点的ID之间以.分隔, 用于定位位置断点. 示例如下:
2: name = 'greet', locations = 1
2.1: where = Greeter`Greeter.Greeter.greet(personNamed: Swift.String) -> () + 127 at Greeter.swift:9:12, address = Greeter[0x00000001000034cf], unresolved, hit count = 0
// 逻辑断点是动态的, 如果程序载入了新的代码, 会自动载入新的位置断点.
2.2.3.3 修改断点
// 修改断点:
// breakpoint modify
// 使用这个命令修改逻辑断点或者位置断点, 需要传递逻辑断点或者位置断点的ID 作为参数, 具体参数可以通过help breakpoint modify 查看.
部分常用选项如下:
--condition(-c) 传入表达式参数, 只有表达式判断为true 才会执行断点.
ignore-count(-i) 指定跳过断点的次数, 在该断点处跳过指定次数后, 才会执行该断点.
--one-shot(-o) 执行断点一次后移除该断点.
--queue-name(-q) 指定队列名称, 该断点只有在指定队列上才会执行.
--thread-name(-T) 指定线程名称, 该断点只有在指定的线程上才会执行.
--thread-id(-t) 指定线程ID(TID), 该断点只有在指定的线程上才会执行.
--thread-index(-x) 指定线程index, 该断点只有在指定的线程上才会执行.
如下示例修改了断点ID为1的断点, 指定其在执行后,自动移除.
(lldb) breakpoint modify --one-shot 1
我们都知道, 当程序抵达断点处时, 程序会暂停执行, 此时可以执行lldb命令.
我们也可以通过执行breakpoint command add 命令, 让程序每次抵达断点处时, 自动执行我们添加的命令. 如:
在位置断点1.1处添加指令, 每行可以添加一个指令, Enter 跳转至下一指令, 输入DONE结束添加.
(lldb) breakpoint command add 1.1
Enter your debugger command(s). Type 'DONE' to end.
> thread backtrace
> DONE
可以将process continue 作为最后一个指令, 那么调试器就会在执行指令后, 自动继续执行程序, 这在记录Log的场景下使用是非常方便的.
(lldb) target create "Greeter"
Current executable set to (x86_64).
(lldb) breakpoint list
No breakpoints currently set.
(lldb) breakpoint set -n greet
Breakpoint 1: where = Greeter`Greeter.Greeter.greet(personNamed: Swift.String) -> () + 127 at Greeter.swift:9:12, address = 0x00000001000034cf
(lldb) breakpoint command add 1
Enter your debugger command(s). Type 'DONE' to end.
> frame variable
> process continue
(lldb) process launch
Process 5997 launched: (x86_64)
2.2.3.4 修改断点状态
设置断点可用状态(Diable/Enable)
禁用断点: breakpoint disable
启用断点: breakpoint enable
以上命令都需要指定断点ID参数(支持通配符).
当逻辑断点设置为diable时, 它下方的所有位置断点都将不再生效.
(lldb) breakpoint disable 1
1 breakpoints disabled.
(lldb) breakpoint disable 2.1
1 breakpoints disabled.
(lldb) breakpoint enable 1
1 breakpoints enabled.
(lldb) breakpoint enable 2.1
1 breakpoints enabled.
还可用通配符*指定断点.
如下示例先是禁用了逻辑断点1下的所有位置断点, 而后启用了位置断点1.1
(lldb) breakpoint disable 1.*
2 breakpoint disabled*
(lldb) breakpoint enable 1.1
1 breakpoint enabled.
2.2.3.5 删除断点
删除断点
(lldb) breakpoint delete 1
1 breakpoints deleted; 2 breakpoint locations disabled.
2.2.4 watchpoint
其与breakpoint很类似,主要用于在感知到内存发生变化后触发断点。内存断点受硬件寄存器数量的限制。
// 针对变量设置内存断点
(lldb) watchpoint set variable places
Watchpoint created: Watchpoint 1: addr = 0x100004a40 size = 8 state = enabled type = w
declare @ 'main.swift:7'
watchpoint spec = 'places'
new value: 1 value
// 针对表达式返回值设置内存断点
(lldb) watchpoint set expression -- (int *)$places + 8
Watchpoint created: Watchpoint 2: addr = 0x100005f33 size = 8 state = enabled type = w
new value: 0x0000000000000000
// 默认情况下,内存断点监视对变量或地址的写访问。 通过传递值为 read、write 或 read_write 的 --watch (-w) 选项来指定对监视器的访问类型。
// 默认情况下,观察点使用目标机器的指针字节大小监视读取和写入。 通过传递值为 1、2、4 或 8 的 --size (-s) 选项,更改用于监视区域的字节数。
内存断点的删除、修改等操作同breakpoint
2.2.5 thread
- n 下一行(next的简写)
- c 继续(continue的简写)
- s 进入函数(step into的简写)
-
finish 跳出当前函数
上面是命令行的输入,对应下面的GUI按钮。
// 返回某值
thread return value
// 跳过2条语句
thread jump --by 2
// 这两个命令也与流程控制类似,不过它们属于frame
up\down
2.2.6 image
// 列出所有依赖库
(lldb) image list
[ 0] 69F6C476-555D-3987-B162-E62C9A1D718C 0x000000010008c000 /var/mobile/Containers/Bundle/Application/D6B51B8B-00F8-443F-9B8E-EF47C2330D2B/WhatsApp.app/WhatsApp (0x000000010008c000)
[ 1] 3134CFB2-F722-310E-A2C7-42AE4DC131AB 0x00000001017ec000 /Library/MobileSubstrate/MobileSubstrate.dylib (0x00000001017ec000)
[ 2] 4DB79D94-6764-3180-99C9-5615E29073E7 0x0000000196848000 /Users/piao/Library/Developer/Xcode/iOS DeviceSupport/9.0.2 (13A452)/Symbols/usr/lib/libSystem.B.dylib
// 查找ASLR偏移
(lldb) image list -f -o WhatsApp
[ 0] /var/mobile/Containers/Bundle/Application/D6B51B8B-00F8-443F-9B8E-EF47C2330D2B/WhatsApp.app/WhatsApp 0x000000000008c000(0x000000010008c000)
ASLR偏移:0x000000000008c000
// 打印加载模块列表
(lldb) image list -f -o // 通常带这两个参数使用
[ 0] 40E417A4-F966-3DB4-B028-B0272DC016A7 0x000a0000 /Users/piao/Library/Developer/Xcode/DerivedData/debug-bdkhskdqykkoqmhjedilckzvpuls/Build/Products/Debug-iphoneos/debug.app/debug
/Users/piao/Library/Developer/Xcode/DerivedData/debug-bdkhskdqykkoqmhjedilckzvpuls/Build/Products/Debug-iphoneos/debug.app.dSYM/Contents/Resources/DWARF/debug
[ 1] 011601C0-F561-3306-846B-94A7C8C841DA 0x2d9e6000 /Users/piao/Library/Developer/Xcode/iOS DeviceSupport/7.1.2 (11D257)/Symbols/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics
查找原始地址信息
在可执行文件或任何共享库中查找原始地址的信息。
// image lookup -a 表达式
例子:
(lldb) image lookup -a $pc
Address: debug[0x0000b236] (debug.__TEXT.__text + 1254)
Summary: debug`main + 58 at main.m:16
//查找类信息
image lookup --type UIViewController
image lookup -t UIViewController
//查找符号
image lookup --name viewDidLoad
image lookup -n viewDidLoad
//查找地址
image lookup --address 0x000000018ff60490
image lookup -a 0x000000018ff60490
三、使用LLDB
3.1 使用LLDB的方式
日常,我们使用LLDB主要是通过Xcode提供的breakpoint navigator、Debug Area(variables view、console)来完成的。其中,在程序已经暂停的情况下,可以使用console来通过交互式命令来完整使用LLDB提供的能力。
从广义的角度来说,第四节 扩展LLDB也属于使用LLDB的方式,不过其与本章的使用具有较大的差异,所以另起一小节来介绍。
作为日常使用,可以仔细看一下下面图中的各个选项、文本框应该填写什么,来更加充分的使用LLDB提供的能力。
下面,介绍更多的日常实际场景的例子。
3.2 实际使用LLDB
3.2.1 修改返回值
- (int)rtInt {
NSLog(@"rtInt");
// 在下面一行设置断点,并添加如下图所示内容(方法1)
int n = 10;
// 在下面一行设置断点,并添加如下图所示内容(方法2)
return n;
}
- (NSDictionary *)rtDict {
NSLog(@"rtDict");
NSDictionary *dict= @{@"key":@"value"};
// 设置断点,然后控制台输入,并在LLDB的环境里定义变量,并把变量赋值给我们需要的对象
// e NSDictionary *$otherDict = @{@"key1":@"value1"}
// e dict = $otherDict
// 在LLDB里可以定义新的变量,变量必须以$开头
return dict;
}
3.2.2 输出布局信息
(lldb) po [self.view recursiveDescription]
<UIView: 0x7fb5b6909b50; frame = (0 0; 393 852); autoresize = W+H; backgroundColor = <UIDynamicSystemColor: 0x6000020e6b40; name = systemBackgroundColor>; layer = <CALayer: 0x6000035cb6a0>>
| <UIButton: 0x7fb5b3f04f60; frame = (10 10; 200 80); opaque = NO; layer = <CALayer: 0x6000035fe260>>
| | <UIButtonLabel: 0x7fb5b3b157b0; frame = (81.6667 29; 37 22); text = '点我'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x6000016d08c0>>
以上只是简单的UI调试,可以使用Xcode提供的Debug View Hierarchy来进行更深入的排查。
[图片上传失败...(image-b0acbc-1670762937746)]
通过这些UI所提供的的调试能力,可以查看被截断的UI、布局以及创建布局的调用堆栈、视图层次、大小等信息。
3.3 修改UI并实时看到
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(10, 10, 200, 80)];
[self.view addSubview:btn];
[btn addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];
[btn setTitleColor:UIColor.redColor forState:UIControlStateNormal];
[btn setTitle:@"点我"forState:UIControlStateNormal];
}
- (void)click:(UIButton *)btn {
// 在这里设断定,并输入如下命令
// e (void)[self.view setBackgroundColor:[UIColor grayColor]] 修改颜色
// e (void)[CATransaction flush] // 把颜色的修改通过渲染更新到界面
NSLog(@"here");
}
注意
以上命令并不总是能成功,在viewDidLoad中设置断点然后进行操作就不行
3.4 注意返回类型
在上面例子中,如果输入
e [self.view setBackgroundColor:[UIColor grayColor]]
则会报这个错误
error: expression failed to parse:
error: <user expression 0>:1:12: no known method '-setBackgroundColor:'; cast the message send to the method's return type
[self.view setBackgroundColor:[UIColor grayColor]]
~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 上面的错误信息告诉LLDB不知道返回类型,所以需要强制类型转换
// 这是在使用LLDB过程中经常碰到的问题
3.5 跳过某些语句
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"%d", __LINE__); // 假设此行是41
NSLog(@"%d", __LINE__); // 在此行设置断点,在debug command中输入:thread jump --by 1
NSLog(@"%d", __LINE__);
NSLog(@"%d", __LINE__);
NSLog(@"%d", __LINE__);
NSLog(@"%d", __LINE__);
// 以下是输出,可以看出跳过了42行
baseStudy[33708:346268] 41
baseStudy[33708:346268] 43
baseStudy[33708:346268] 44
baseStudy[33708:346268] 45
baseStudy[33708:346268] 46
}
3.6 命令后执行
// 这是上面设置的命令
// -C 表示后面可以添加新的命令
// -G true标识断点不停止,直接运行
br set -l 43 -f ViewController.m -C 'po @"onthway"' -G true
// 可以看到断点其实被设置3个(因为循环了3次)
5: file = 'ViewController.m', line = 43, exact_match = 0, locations = 1, resolved = 1, hit count = 2 Options: enabled auto-continue
Breakpoint commands:
po @"onthway"
5.1: where = baseStudy`-[ViewController viewDidLoad] + 90 at ViewController.m:43:9, address = 0x000000010d564cba, resolved, hit count = 2
6: file = 'ViewController.m', line = 43, exact_match = 0, locations = 1, resolved = 1, hit count = 1 Options: enabled auto-continue
Breakpoint commands:
po @"onthway"
6.1: where = baseStudy`-[ViewController viewDidLoad] + 90 at ViewController.m:43:9, address = 0x000000010d564cba, resolved, hit count = 1
7: file = 'ViewController.m', line = 43, exact_match = 0, locations = 1, resolved = 1, hit count = 0 Options: enabled auto-continue
Breakpoint commands:
po @"onthway"
7.1: where = baseStudy`-[ViewController viewDidLoad] + 90 at ViewController.m:43:9, address = 0x000000010d564cba, resolved, hit count = 0
3.7 执行一次的断点
// 把上面的断点语句添加--one-shot true
br set --one-shot true -l 43 -f ViewController.m -C 'po @"onthway"' -G true
// 输出3次 ontheway
3: file = '/Users/wangwenjun04/Desktop/myfolder/studyCode/baseStudy/baseStudy/ViewController.m', line = 43, exact_match = 0, locations = 1, resolved = 1, hit count = 3
3.1: where = baseStudy`-[ViewController viewDidLoad] + 90 at ViewController.m:43:9, address = 0x0000000103ea0cba, resolved, hit count = 3
4: file = '/Users/wangwenjun04/Desktop/myfolder/studyCode/baseStudy/baseStudy/ViewController.m', line = 50, exact_match = 0, locations = 1, resolved = 1, hit count = 1
4.1: where = baseStudy`-[ViewController viewDidLoad] + 199 at ViewController.m:50:22, address = 0x0000000103ea0d27, resolved, hit count = 1
// 此断点只其一次作用
7: file = 'ViewController.m', line = 43, exact_match = 0, locations = 1, resolved = 1, hit count = 0 Options: enabled one-shot auto-continue
Breakpoint commands:
po @"onthway"
7.1: where = baseStudy`-[ViewController viewDidLoad] + 90 at ViewController.m:43:9, address = 0x0000000103ea0cba, resolved, hit count = 0
3.8 忽略特定次数
假如有以下代码,当i的值是9的时候会出现问题。那么按照上述基本的调试方法,在if这一行设置断点,然后要通过8次的暂停才能在我们想要的地方停住。
NSInteger count = array.count;
for (NSInteger i = 0; i < count; i++) {
if (i % 2) {
// 做一些事情
} else {
// 做另外一些事情
}
}
通过设置跳过前8次执行,LLDB自动在我们想要的时候暂停程序的执行。通过设置ingore的次数,我们的调率得到了提升。
3.9 条件断点
假如我们有如下代码:
void printName(NSString *name) {
NSLog(@"name=%@", name);
}
int main(int argc, const char * argv[]) {
printName(@"jzm");
printName(@"xjp");
printName(@"hjt");
}
我们的需求是在name是xjp时暂停,那么按照简单的调试方法,只能在NSLog那一行来设置断点,然后再每次断点停住的时候检查name的值是否是xjp。我们可以更家搞笑,通过设置条件断点的方式。
3.10 记录日志
很多时候,我们是需要记录程序执行过程的路径、变量的值、次数等来发现问题,可以通过如下的方式来记录日志。
[图片上传失败...(image-e28259-1670762937746)]
除此方式外,还可以通过expression表达时候来记录日志。
[图片上传失败...(image-fb046a-1670762937746)]
3.11 过滤日志
有些情况下,我们添加的日志、程序的输出过多,会淹没我们想要看到的内容,可以通过过滤日志来实现。
还可以通过mac提供的控制台程序来实现多字段的过滤。
四、扩展LLDB
LLDB、Xcode提供了便捷的方式进行扩展,这些扩展都基于LLDB提供的接口。
3.1 工程级扩展
我们可以专门针对某个工程进行扩展,其方法很简单。新建一个用于扩展LLDB的python文件,然后选择edit scheme进行配置。
setting set prompt [wwj-lldb]$
command alias wflush e (void)[CATransaction flush]
# 修改提示语
setting set prompt [wwj-lldb]$
# 新建一个命令别名,用于刷新UI
command alias wflush e (void)[CATransaction flush]
3.2 系统级扩展
在~/.lldbinit文件中添加自定义命令,如果没有此文件需要新建。
FaceBook开源的chisel就是采用的此种方式。
其提供了许多扩展命令。
Command | Description | iOS | OS X |
---|---|---|---|
pviews | Print the recursive view description for the key window. | Yes | Yes |
pvc | Print the recursive view controller description for the key window. | Yes | No |
visualize | Open a UIImage , CGImageRef , UIView , CALayer , NSData (of an image), UIColor , CIColor , or CGColorRef in Preview.app on your Mac. |
Yes | No |
fv | Find a view in the hierarchy whose class name matches the provided regex. | Yes | No |
fvc | Find a view controller in the hierarchy whose class name matches the provided regex. | Yes | No |
show/hide | Show or hide the given view or layer. You don't even have to continue the process to see the changes! | Yes | Yes |
mask/unmask | Overlay a view or layer with a transparent rectangle to visualize where it is. | Yes | No |
border/unborder | Add a border to a view or layer to visualize where it is. | Yes | Yes |
caflush | Flush the render server (equivalent to a "repaint" if no animations are in-flight). | Yes | Yes |
bmessage | Set a symbolic breakpoint on the method of a class or the method of an instance without worrying which class in the hierarchy actually implements the method. | Yes | Yes |
wivar | Set a watchpoint on an instance variable of an object. | Yes | Yes |
presponder | Print the responder chain starting from the given object. | Yes | Yes |
... | ... and many more! |
其在~/.lldbinit中添加了如下的内容
# ~/.lldbinit
command script import /usr/local/opt/chisel/libexec/fbchisellldb.py
对此感兴趣的读者可以对chisel进行深入研究。
3.3 高阶扩展
通过LLVM工程自己编译LLDB代码(难度高,有兴趣的高手可以尝试并分享给大家)。