[转] Xcode 高级调试技巧

Posted linganxiong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[转] Xcode 高级调试技巧相关的知识,希望对你有一定的参考价值。

在苹果的官方文档中列出了我们在调试中能用到的一些命令,我们在这重点讲一些常用的命令
调试本地文件方法(Mac OS X):(lldb) target create "/Users/piaoyun/Desktop/xx.app/Contents/MacOS/xxxx"
远程调试方法:
设备端运行:
附加进程:
./debugserver *:1234 -a "YourAPPName"
直接启动进程:
debugserver -x backboard *:1234 /path/to/app/executable
例:
debugserver -x backboard *:1234 /Applications/MobileNotes.app/MobileNotes
此命令会启动记事本,并断在dyld的第一条指令上
在Mac终端运行lldb命令后,输入以下2条命令:
platform select remote-ios
process connect connect://你的设备IP地址:1234
用USB连接方法:
////////////////////////////////////////////////////////////////////////////////////////
wget http://cgit.sukimashita.com/usbmuxd.git/snapshot/usbmuxd-1.0.8.tar.bz2
tar xjfv usbmuxd-1.0.8.tar.bz2
cd usbmuxd-1.0.8/Python-client/
python tcprelay.py -t 1234:1234
在Mac终端运行lldb命令后,输入以下2条命令:
platform select remote-ios
process connect connect://localhost:1234
////////////////////////////////////////////////////////////////////////////////////////
 -(void)loginWithUserName:(NSString *)username password:(NSString *)password
{
    NSLog(@"login.... username:%@   password:%@", username, password);  // 假设我们在此下断点
}
一、基本操作
1.1.视图层次
打印视图层次 po [self.contentView recursiveDescription]
1.2.改变某个取值
1. int a = 1;
2. //Console expr a=2
3. NSLog(@"实际值: %d", a);
1.3.call 改变view的背景色
call [self.view setBackgroundColor:[UIColor redColor]] 
1.4.声明变量
1. expr int $b=2 或者 e int $b=2
2. //输出  po $b
print (type)表达式
例子:
print (int)$r6
print username
1.5.打印堆栈
ios中打印堆栈方法是 NSThread callStackSymbols,这里调试的时候有个简单的方法如下
bt  或者  bt all 
1.6.更改方法返回值
thread return NO/YES
1. -(BOOL) returnYES
2. {
3.     //thread return NO ,可以更改函数返回值
4.     return YES;
5. }//方法返回结果为NO  
1.7.多线程异常后查看历史对象的malloc分配历史
先打开Enable Zombie Objects 和 Malloc Stack
1. (lldb) command script import lldb.macosx.heap
2. (lldb) malloc_info -S 0x7ff7206c3a70 
1.8.寄存器查找对象
曾经遇到过一个问题,[self.tableview reloadData]直接奔溃。这时候tableview其实没有问题,我们要怎么去找问题呢?
 
如上图所示,最后我们通过malloc_info -S 0x00007fe99a629560来查看对象分配在堆里面的具体的地址,随后在左侧打开table的所有变量,输入这个地址即能够看到这个是什么成员。
1.9.  c
继续执行
7.s 
源码级别单步执行,遇到子函数则进入
8.si
单步执行,遇到子函数则进入
9.n 
源码级别单步执行,遇到子函数不进入,直接步过
10.ni
单步执行,遇到子函数不进入,直接步过
11.finish/f
退出子函数
12.thread list
 
打印线程列表
 
13.image lookup -a 表达式、image list
例子:
image lookup -a $pc
返回如下:
      Address: debug[0x0000b236] (debug.__TEXT.__text + 1254)
      Summary: debug`main + 58 at main.m:16
 
 
打印加载模块列表
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 -r -n <FUNC_REGEX>
对于无调试符号的这样使用:
image lookup -r -s <FUNC_REGEX>
 
14.disassemble -a 地址
例子:
dis -a $pc
debug`main at main.m:14:
   0xa71fc:  push   {r7, lr}
   0xa71fe:  mov    r7, sp
   0xa7200:  sub    sp, #0x24
   0xa7202:  movs   r2, #0x0
   0xa7204:  movt   r2, #0x0
   0xa7208:  str    r2, [sp, #0x20]
   0xa720a:  str    r0, [sp, #0x1c]
   0xa720c:  str    r1, [sp, #0x18]
   0xa720e:  blx    0xa7fe0                   ; symbol stub for: 
.
.
.
2015-04-29 添加
disassemble -A thumb    
可选:
thumbv4t
thumbv5
thumbv5e
thumbv6
thumbv6m
thumbv7
thumbv7f
thumbv7s
thumbv7k
thumbv7m
thumbv7em
///////////////////////////////////////////////
 
15.memory read [起始地址 结束地址]/寄存器 -outfile 输出路径
例子:
memory read $pc
0x00035ebe: 0e 98 07 99 09 68 08 9a 90 47 0c 99 03 90 08 46  .....h...G.....F
0x00035ece: 03 99 01 f0 80 e8 02 22 c0 f2 00 02 41 f2 52 10  ......."....A.R.
memory read 0x35f1c 0x35f46 -outfile /tmp/test.txt  // 将内存区域保存到文件
2015-04-29添加:
默认情况下,memory read 只能读取 1024字节数据
例如:
 就会报错
error: Normally, ‘memory read‘ will not read over 1024 bytes of data.
解决方法:加-force参数
memory read 0x1000 0x3000 -outfile /tmp/test.txt -force
或者:
memory read 0x1000 -outfile /tmp/test.txt -count 0x2000 -force
memory read $x0(寄存器) -outfile /tmp/test.txt -count 0x2000 -force
--binary // 二进制输出
例:
memory read 0x1000 0x3000 -outfile /tmp/test.bin --binary -force
写内存:
memory write $rip 0xc3
memory write $rip+1 0x90
16.register read/格式、register write 寄存器名称 数值
例子:
register read/x
返回如下:
General Purpose Registers:
        r0 = 0x1599e028
        r1 = 0x38131621  libobjc.A.dylib`objc_msgSend + 1
        r2 = 0x000a85cc  "class"
        r3 = 0x000a85d4  (void *)0x000a8618: AppDelegate
        r4 = 0x00000000
        r5 = 0x000a71fd  debug`main + 1 at main.m:14
        r6 = 0x00000000
        r7 = 0x27d63c80
        r8 = 0x27d63c98
        r9 = 0x00000002
       r10 = 0x00000000
       r11 = 0x00000000
       r12 = 0x3a3ff1c8  (void *)0x3875cc19: _Unwind_SjLj_Unregister + 1
        sp = 0x27d63c5c
        lr = 0x38136eaf  libobjc.A.dylib`objc_autoreleasePoolPush + 311
        pc = 0x000a7236  debug`main + 58 at main.m:16
      cpsr = 0x20000030
// 改写r9寄存器例子:
register write r9 2
 
17.display 表达式     undisplay 序号
例子:
display $R0
undisplay 1
 
18 内存断点 watchpoint set expression 地址    /  watchpoint set variable 变量名称 -- (源码调试用到,略过)
例子:
watchpoint set expression 0x1457fa70
命中后得到结果:
Watchpoint 3 hit:
old value: 3
new value: 4
18.2 内存访问断点 watchpoint set expression -w read -- 内存地址
watchpoint set expression -w read -- 0x16b9dd91
18.2 内存写入断点 watchpoint set expression -w write -- 内存地址
watchpoint set expression -w read -- 0x16b9dd91
18.3 条件断点 watchpoint modify -c 表达式
例子:
watchpoint modify -c ‘*(int *)0x1457fa70 == 20‘
命中后得到结果:
Watchpoint 3 hit:
old value: 15
new value: 20
19.找按钮事件 po [按钮名称/内存地址 allTargets] 
例子:
(lldb) po [[self btnTest] allTargets]
{(
    <ViewController: 0x166af1f0>
)}
(lldb) po [[self btnTest] actionsForTarget:(id)0x166af1f0 forControlEvent:0]
<__NSArrayM 0x165b8950>(
testAction:
)
Bash
1. // 在机器上实战一下:
2. (lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
3. <UIWindow:
4. 0x15e771c0; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 
5. 0x15e96210>; layer = <UIWindowLayer: 0x15e988e0>>
6.    | <UIView: 0x15eb4180; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x15eb4300>>
7.   
8. |    | <UIButton: 0x15eb32d0; frame = (277 285; 46 30); opaque = NO;
9. autoresize = RM+BM; layer = <CALayer: 0x15eb2e30>>
10.    |    
11. |    | <UIButtonLabel: 0x15db5220; frame = (0 6; 46 18); text = 
12. ‘Button‘; opaque = NO; userInteractionEnabled = NO; layer = 
13. <_UILabelLayer: 0x15db5410>>
14.    |    | <_UILayoutGuide: 0x15eb4360; frame = (0 0; 0 20); hidden = YES; layer = <CALayer: 0x15eb4540>>
15.    |    | <_UILayoutGuide: 0x15eb4af0; frame = (0 568; 0 0); hidden = YES; layer = <CALayer: 0x15eb4b70>>
16.  
17. (lldb) po [(UIButton *)0x15eb32d0 allTargets]
18. {(
19.     <ViewController: 0x15e93250>
20. )}
21.  
22. (lldb) po [(UIButton *)0x15eb32d0 allTargets]
23. {(
24.     <ViewController: 0x15e93250>
25. )}
26.  
27. (lldb) po [(UIButton *)0x15eb32d0 actionsForTarget:(id)0x15e93250 forControlEvent:0]
28. <__NSArrayM 0x15dbfc50>(
29. testAction:
30. )
31. // 调用--
32. (lldb) po [0x15e93250 testAction:nil]
33. 0x00210c18
// 再来一发,对按钮属性操作
Bash
1. (lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
2. <UIWindow: 0x15e771c0; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x15e96210>; layer = <UIWindowLayer: 0x15e988e0>>
3.    | <UIView: 0x15eb4180; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x15eb4300>>
4.    |    | <UIButton: 0x15eb32d0; frame = (277 285; 46 30); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x15eb2e30>>
5.    |    |    | <UIButtonLabel: 0x15db5220; frame = (0 6; 46 18); text = ‘Button‘; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x15db5410>>
6.    |    | <_UILayoutGuide: 0x15eb4360; frame = (0 0; 0 20); hidden = YES; layer = <CALayer: 0x15eb4540>>
7.    |    | <_UILayoutGuide: 0x15eb4af0; frame = (0 568; 0 0); hidden = YES; layer = <CALayer: 0x15eb4b70>>
8.  
9. (lldb) expression UIButton *$btn = (UIButton *)0x15eb32d0
10. (lldb) expression [$btn setHidden:YES]
带参数运行:
Bash
1. (lldb) target create "/bin/ls"
2. Current executable set to ‘/bin/ls‘ (x86_64).
3. (lldb) set set target.run-args  $(python -c ‘print "a"*200‘)
4. (lldb) run
5. Process 40752 launched: ‘/bin/ls‘ (x86_64)
6. ls: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: No such file or directory
7. Process 40752 exited with status = 1 (0x00000001)
8. (lldb)
std::string 读取方式:从内存+8的地方开始  64bit自行变通
例:
Bash
1. (lldb) x $r0+8      
2. 0x02db9248: 20 82 e3 14 71 00 00 00 61 00 00 00 c0 82 d3 14   .惝q...a...喇赢
3. 0x02db9258: 71 00 00 00 2b 00 00 00 20 f9 e6 14 61 00 00 00  q...+... .a...
4. (lldb) x/s 0x14e38220
5. 0x14e38220: "hello!piaoyun"
常见问题-打印无效
上面我们简单的学习了如何使用LLDB命令。但有时我们在使用这些LLDB命令的时候,依然可能会遇到一些问题。不明类型或者类型不匹配
1. p (void)NSLog(@"%@",[self.view  viewWithTag:1001])    //记住要加void 
2. p (CGRect)[self.view frame]  //记住不能写成 self.view.frame,lldb的bug
二、调试进阶
2.1.监听某个方法的调用
  
如果是自定义的view,比如QQView,想监听frame变化,直接[QQView setFrame:]即可 系统方法就要如图所示,x86_64系统中,rdi表示第一个参数,具体其他平台可看inspecting-obj-c-parameters-in-gdb,里面有详细的说明
1. $rdi ? arg0 (self)
2. $rsi ? arg1 (_cmd)
3. $rdx ? arg2
4. $rcx ? arg3
5. $r8 ? arg4
6. $r9 ? arg5
2.2.image寻址,找到崩溃行
此时会调用如下代码会崩溃
1. NSArray *arr=[[NSArray alloc] initWithObjects:@"1",@"2", nil];
2. NSLog(@"%@",arr[2]);
3.  
4. 2015-06-30 14:47:59.280 QQLLDB[6656:138560] *** Terminating app due to uncaught exception ‘NSRangeException‘, reason: ‘*** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]‘
5. *** First throw call stack:
6. (
7. 0   CoreFoundation                      0x000000010b2d8c65 __exceptionPreprocess + 165
8. 1   libobjc.A.dylib                     0x000000010af71bb7 objc_exception_throw + 45
9. 2   CoreFoundation                      0x000000010b1cf17e -[__NSArrayI objectAtIndex:] + 190
10. 3   QQLLDB                              0x000000010aa404f6 -[ViewController viewDidLoad] + 1030
11. 4   UIKit                               0x000000010ba75210 -[UIViewController loadViewIfRequired] + 738
12. 5   UIKit                               0x000000010ba7540e -[UIViewController view] + 27
13. 6   UIKit                               0x000000010b9902c9 -[UIWindow 
此时我们要直接找到崩溃的行数,很简单。先找到非系统bug的崩溃地址,如下图所示,显然是第三行QQLLDB,右边对应的地址是0x000000010aa404f6,然后输入image lookup --address 0x000000010aa404f6,即可看到是60行出了bug
1. image lookup --address 0x000000010aa404f6  
2. Address: QQLLDB[0x00000001000014f6] (QQLLDB.__TEXT.__text + 1030)  
3. Summary: QQLLDB`-[ViewController viewDidLoad] + 1030 at ViewController.m:60
2.3.crash日志分析,读取符号表
1.要知道如何读取符号表,我们得先伪造一份符号表的数据文件出来,代码如下
1. NSArray *arr=[[NSArray alloc] initWithObjects:@"1",@"2", nil];
2. NSLog(@"%@",arr[2]);
伪造步骤:
1.编译到真机
2.然后进入xcode将这个打开,找到QQLLDB.app.dSYM这个文件,偷偷拷贝一份到桌面
3.然后在product中clean一下工程
4.在真机上打开一个编译进去的程序,会出现图
步骤如下图所示:
 
 
  
  
 
之后在命令行操作
1. atos -o /Users/tomxiang/Desktop/符号表/QQLLDB.app.dSYM/Contents/Resources/DWARF/QQLLDB -l0x1000e8000 0x1000eebd4
2.     //此时得到结果,告诉我们是ViewController的viewDidLoad的第62行崩溃
3. -[ViewController viewDidLoad] (in QQLLDB) (ViewController.m:62)
2.4观察实例变量的变化
假设你有一个 UIView,不知道为什么它的 _layer 实例变量被重写了 (糟糕)。因为有可能并不涉及到方法,我们不能使用符号断点。相反的,我们想监视什么时候这个地址被写入。
首先,我们需要找到 _layer 这个变量在对象上的相对位置:
1. (lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar*)class_getInstanceVariable([MyView class], "_layer"))
2. (ptrdiff_t) $0 = 8
现在我们知道 ($myView + 8) 是被写入的内存地址:
1. (lldb) watchpoint set expression -- (int *)$myView + 8
2. Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
3. new value: 0x0000000000000000
这被以 wivar $myView _layer 加入到 Chisel 中。
三、Chisel-facebook开源插件
1.安装方法:
1. git clone https://github.com/facebook/chisel.git ~/.chisel
2. echo "command script import ~/.chisel/fblldb.py">>~/.lldbinit
安装好后,打开xcode就可以运行调试了
2.基本命令
2.1.预览图片
1. UIImage *image = [UIImage imageNamed:@"clear"];
2. UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
3. [viewB addSubview:imageView];
4.  
5. //此时执行命令之后,图片会被苹果用自带的预览工具显示出来    
6. visualize image
2.2.边框/内容着色
如下所示,打印得到imageView地址之后,然后用border命令将其边框着色unborder取消着色
1. (lldb) p imageView
2. (UIImageView *) $2 = 0x00007fb8c9b15910
3. (lldb) border 0x00007fb8c9b15910 -c green -w 2
同理,mask是给内容着色 unmask
mask imageView -c green 
2.3.关系链的继承
1. (lldb) pclass image
2. UIImage
3.     | NSObject
2.4.打印所有属性
1. `pinternals`这个命令就是打印出来的一个控件(id)类型的内部结构,详细到令人发指!甚至是你自定义的控件中的类型,譬如这个styleView就是我自定义的,内部有个iconView的属性,其中的值它也会打印出来。   
2.  
3. (lldb) pinternals image
4. (UIImage) $8 = {  
5.     NSObject = {
6.     isa = UIImage  
7. }
8. _imageRef = 0x00007fc1f3330780
9. _scale = 2
10. _imageFlags = {
11. named = 1
12. imageOrientation = 0
13. cached = 0
14. hasPattern = 0
15. isCIImage = 0
16. renderingMode = 0
17. suppressesAccessibilityHairlineThickening = 0
18. hasDecompressionInfo = 0
19. }
20. }
2.5.bmessage
如果Chisel
ViewController没有设置viewWillDisappear这个方法,此时我想用断点断下来,可以这样
1. (lldb) bmessage -[ChiselViewController viewWillDisappear:]
2.  
3. Setting a breakpoint at -[UIViewController viewWillDisappear:] with condition (void*)object_getClass((id)$rdi) == 0x0000000100c47060
4. Breakpoint 2: where = UIKit`-[UIViewController viewWillDisappear:], address = 0x0000000101a0b566
Breakpoints
BreakPoint分类
breakpoint也是有分类的,我这里的文章内大致按使用的方式分为了
? Normal Breakpoint,
? Exception Breakpoint,
? OpenGL ES Error breakpoint,
? Symbolic Breakpoint,
? Test Failure Breakpoint,
? WatchPoints。
 
可以按具体的情景使用不同类型的breakpoint,解决问题为根本。
Normal Breakpoint
添加普通断点就不多说了,在源代码的右侧点击一下即可。或者,使用快捷键:command + 来添加和删除。这两种方式添加的breakpoints在Xcode上面是可以通过UI看到的。
还有可以通过下面两个LLDB命令直接在运行时添加断点,但是这种方式需要注意的是一方面无法通过UI直接看到断点,另外一方面只存在于本次运行,下一次启动模拟器重新运行的时候,这些断点就不生效了。
 
如上图,通过“br li”命令打印所有的breakpoint,可以看到一共有3个breakpoint,第一个是通过Xcode的UI添加的,后面两个分别是通过下面两个命令添加的:
“breakpoint set -f XXX.m -l XX” 和  “b XXX.m:XX”。
breakpoint set -n write -c "(*(char**) ($esp + 8))[0]==0x17      && (*(char**) ($esp + 8))[1]==0x03      && (*(char**) ($esp + 8))[2]==0x03      && (*(char**) ($esp + 8))[3]==0x00     && (*(char**) ($esp + 8))[4]==0x28"
Exception Breakpoint
可以通过下图中Xcode的UI添加Exception Breakpoint。有时候,比如数组越界或者设置一个空对象等问题,都会抛出一个异常,但是这种类型的错误非常难以定位,这个时候就可以使用Exception Breakpoint来进行调试,在异常发生时可以捕捉到并停止程序的执行。OC中的异常是一个常被忽略的地方,但实际上系统框架内这个使用非常广泛,大部分这种错误信息,系统框架都会以异常的形式throw出来,所以善用这种breakpoint的话,我们能大大减少查找错误的时间。
 
例如,当我们添加如下Exception Breakpoint之后(bt 命令后文中会讲解,这个命令的作用是在断点触发时,打印回调栈信息):
 
类似下面这样的数组越界的问题,我们可以很容易就定位到问题所在,不用再毫无头绪找来找去了:
 
当断点暂停执行时,我们可以通过Xcode的UI中查看调用栈信息:
 
或者查看bt命令打印的调用栈信息:
 
还有类似如下的错误可以通过这种断点很容易定位到:
 
,不过这种问题,可以通过使用setValue:forKey:代替来避免。
OpenGL ES Error Breakpoint
同上图中,在Xcode的breakpoint navigator的下部添加按钮,选择”Add OpenGL ES Error Breakpoint”即可。这个breakpoint主要是用来在OpenGL ES发生错误时停止程序的运行。
Symbolic Breakpoint
通过Xcode的UI添加symbolic breakpoint的方式同exception breakpoint,弹出框如下:
 
Symbolic breakpoints 在某个特定的函数或者方法开始执行的时候,暂停程序的执行,通过这种方式添加断点,我们就不需要知道在源文件中添加,也不需要知道断点设置在文件的第几行。
上图中,最主要的设置是Symbol的内容,可以有如下几种:
? 1. A method name,方法名称,例如 pathsMatchingExtensions: 这样的方法名称,会对所有类的这个方法都起作用。
? 2. A method of a particular class. 特定类的某个方法。例如 ,[SKLine drawHandlesInView],或者 people::Person::name()
? 3. A function name。函数名称。例如 ,_objc_msgForward 这样C函数。
另外,也可以通过命令行的方式添加 Symbolic breakpoints。对C函数添加断点:
 
对OC的方法添加断点:
 
常用的这个类型的断点有,objc_exception_throw可以用来代替 Exception Breakpoint,还有一个-[NSObject doesNotRecognizeSelector:] 也比较常用,用于检测方法调用失败。
下断:
breakpoint set -a 函数地址   --常规断点
breakpoint set --func-regex 函数关键字   --飘云提示:这个非常有用!我也是最近才研究发现的-虽然官方文档一直有,但是没重视
这样下断的优势:
比如再某动态库中有 testA函数,那么常规做法是先 image list -o -f 查看模块基址 然后 image lookup -r -n 函数关键字找到偏移   然后再 br s -a 基址+偏移!
用上面这个命令下端就简洁方便了!!!lldb会自动帮你下断所有匹配特征字的断点,可以模糊匹配哦
再来一个对动态库函数下断的:
breakpoint set --shlib foo.dylib --name foo
这个也非常有用,可以进行断点过程中的一些自动化处理:
breakpoint command add 断点序号
这个也非常有用,对C函数下断非常好 / 貌似是模糊匹配
breakpoint set -F isTest   / 可以简写为 b isTest
Test Failure Breakpoint
通过Xcode的UI添加方法同上。这个类型的break point 会在 test assertion 失败的时候暂停程序的执行。
Watchpoints
Watuchpoints是一个用来监听变量的值的变化或者内存地址的变化的工具,发生变化时会在debugger中触发一个暂停。对于那些不知道如何准确跟踪的状态问题,可以利用这个工具来解决。要设置watchpoint的话,在程序运行到stack frame包含有你想观察的变量时,让debugger暂停运行,这个时候变量在当前stack frame的scope内,这个时候才能对该变量设置watchpoint。
你可以在Xcode的GUI中设置watchpoint,在xcode的 Variables View中,把你想观察的变量保留出来,然后右键设置“Watch XXX”。例如下图,观察self的title变量,点击 Watch “_button1ClickCount” 即可。
 
命令行
或者也可以通过命令行来设置watchpoint:watch set variable _button1ClickCount,详细命令可以参考:http://lldb.llvm.org/lldb-gdb.html,有好几种命令可以达到同样的效果。
上面是对变量进行观察,实际上我们可以对任意内存地址进行观察,命令如下:watchpoint set expression — 0x123456,参考:http://stackoverflow.com/questions/21063995/watch-points-on-memory-address
需要注意的是,watchpoint是分类型的,包括read,write或者read_write类型,这个非常容易理解,在读,写或者读写变量或内存的时候,watchpoint是否被触发。read,write或read_write跟着-w参数后面表示类型。另外,命令行中,watchpoint还有一些简写,set简写为s,watch简写为wa,variable简写为v。
下面的示例是来自 http://www.dreamingwish.com/article/lldb-usage-a.html 网站的几个命令:
 
第一个命令是监听_abc4变量的内存地址write的变化,第二个是监听_abc4变量read的变化,第三个是监听_abc3变量read_write的变化。
需要注意的是,通过Xcode的GUI添加的watchpoint为默认类型,即write类型,如果想要添加读写都watch的watchpoint,则只能通过命令行工具进行添加了。
使用watchpoint modify -c ‘(XXX==XX)’,则修改watchpoint之后在某个值的时候才会监听。
编辑选项
BreakPoint Condition
当我们通过Xcode对breakpoint进行编辑时,可以发现normal breakpoint和symbolic breakpoint都有一个”Condition”输入选项,这个的作用很容易理解,只有在设置的condition表达式为YES的情况下这些断点才会起作用。
例如,下图中的breakpoint在判断字符串相等的时候才会停止运行:
 
可以注意到这里使用stringWithUTF8Stirng:方法,原因在于lldb的expression parser有一个bug,不兼容非ASCII字符,需要处理一下才行,否则会报错“An Objective-C constant string‘s string initializer is not an array”,参考:http://stackoverflow.com/questions/17192505/error-in-breakpoint-condition
更加简单一些的例子就不说了,比如 i == 99之类的简单比较,只要表达式的结果为BOOL类型即可。
Breakpoint Actions
可以看到上面的每种breakpoint编辑选项中基本上都有“Add Action”选项,当breakpoint被触发时,都首先会执行我们设置的这些action,然后我们才能得到控制权,即Xcode上面才会显示程序停止执行的UI。这个Action通过例子比较好理解,我们通过上面那个setObject:forKey:的异常来说明。代码如下:
 
设置Breakpoint:
 
可以看到上图中,我们一共设置了3个action。第一个action,用来打印exception的详细信息,用法参考:http://stackoverflow.com/questions/17238673/xcode-exception-breakpoint-doesnt-print-details-of-the-exception-being-thrown。
第二个action,我们使用shell命令“say”,让电脑发声,把一段文字读出来。
第三个action,我们使用“bt”命令来打印调用栈信息
设置完成之后,当异常发生时,我们会听到电脑发声念上图中的英文,然后在log中可以看到如下信息,第一行是Exception的描述信息,下面是调用堆栈:
 
Continuing after Evaluation
看一下breakpoint的编辑弹窗,我们可以发现有一个 “Automatically continue after evaluation actions” checkbox选项。当我们勾选这个checkbox之后,debugger会执行breakpoint中添加的所有的actions,然后继续执行程序。对于我们来说,除了触发一大堆command并且执行时间很长的情况之外,程序会很快跳过这个breakpoint,所以我们可能根本不会注意到这个breakpoint的存在。所以,这个选项的功能相当于在执行的最后一个action之后,直接输入continue命令继续执行。
有了这个很强大的功能,我们可以直接通过breakpoints来单独对我们的程序进行修改。在某行代码时停止执行,使用”expression”命令来直接修改程序的某个变量设置直接修改UI,然后继续执行。expression / call 配合这个选项的时候,会非常强大,可以很方便实现很多很强大的功能。
例如,我们实现一个如下的功能,把tableview的第一个cell的selectBackgroundView的背景色改为红色:
 
action的内容为“expression [[cell selectedBackgroundView] setBackgroundColor:[UIColor redColor]]”,这里的表达式先不用关心,我们后面LLDB章节会讲到,修改之后,当我们点击cell的时候,cell的背景就会如下图一样变红:
 
使用这种方式,我们在不需要修改一行代码的情况下,只需要通过修改breakpoint,就可以实现对UI的各种调试效果。
 
前言
你是否呕心沥血的尝试去理解代码和打印出来的变量内容?
NSLog(@"%@", whatIsInsideThisThing); 
或是漏过函数调用来就简化工程行为?
NSNumber *n = @7; // theFunctionThatShouldReallyBeCalled(); 
或者短路的检查逻辑?
if (1 || theBooleanAtStake) { ... } 
亦或者是函数的伪实现?
1. int calculateTheTrickyValue {
2.   return 9;
3.  
4.   /*
5.    Figure this out later.
6.    ...
7. }
8.
那是不是要不断的重编译,然后又开始新的轮回?
构建软件是复杂的而且BUG无处不藏。一个正常的修正过程是修改代码,编译,再次运行,然后祈祷上帝。
似乎也不用墨守成规。你可以用调试器啊!假设你已经知道怎么检视变量值,这里有更多你需要掌握的东西。
这篇文章的目的是挑战你的调试知识,把你可能知道得基础知识点解析的更透彻,然后向你展示了一系列有趣的栗子。开始吧!
LLDB
LLDB是个开源调试器,REPL特性,自带C++以及Python插件。它与Xcode绑定并且驻在控制台界面化于窗口的下端。
调试器允许你在一个特定执行时刻暂停程序,检视变量值,执行自定义命令,以及按你认为合适得步骤进行程序步骤操控。(调试器主要功能戳这里)
你以前使用调试器的部分很可能仅限于Xcode的UI上打个断点。但是这有些技巧,你可以做一些更酷比的事情。通过GDB与LLDB之间对比是针对所有支持的命令行的一个很好鸟瞰式的学习法,你还可能想要去安装Chisel,一套开源的LLDB插件让你的调试更加有趣。
与此同时,让我们开始如何使用调试器打印变量值的旅程吧。
基础
这里有一个简单短小的程序来打印字符串。注意到断点被添加到了第八行:
 
程序到此会停下来然后打开控制台,让我们能与调试器进行交互。此时我们应该输入什么呢?
帮助
最简单得命令是键入help,你可以获取一个命令行列表。如果你忘记一个命令或者想知道该命令更细致的使用方法,那么你可以通过调用help <command>,比如help print或help thread。如果你甚至忘记了命令本身,你可以尝试使用help help,但是如果你懂得足够多,你可能已经彻底不要这个命令了。
打印
打印值很容易,只要试着键入print命令:
 
LLDB实际上支持前缀命令判断,所以你同样可以使用prin, pri或者p。但是你不能使用pr,因为LLDB不能分辨出你是否是想执行process命令。(吐槽幸好p没有歧义,暴露属性)
你同时也注意到了结果带一个$0。实际上你可以用这个来引用变量!试着键入$0 + 7然后你会看到106。任何带美元符号是LLDB的命名空间,其存在是为了为你提供帮助。
表达式
如果你想修改一个值?修改,你说的算?好吧,修改!下面来一个简单得表达式命令行:
 
这并不修改调试器中的值。实际上修改的是程序中的值!如果你继续程序,它很神奇地会打印出42红气球(上下文)。
从现在开始注意一点,我们为了方便用p与e代替print和expression。
什么是打印命令?
这里有一个有意思的表达式来考虑下:p count = 18。如果我们执行命令然后打印count的内容,我们会看到它确实相当于执行了表达式count = 18。
这两者的区别是print命令不带参数,这点与expression不同。考虑e -h +17。在选择是否要进行输入源为+17,带-h标志的操作,还是选择是否要进行计算区分17和h操作,在这两个选择上面是不明确的。调试器认为连字符导致了混淆,你可能得不到想要的结果。
幸运的是,这个解决方法十分简单。使用--来表示表示符号的结束以及输入源的开始。此时如果你想要用-h标志,你可以使用e -h -- +17,如果你想要进行区分,则你可以执行e -- -h +17。不带标志则是十分普通,它(e --)有一个别名print。
如果你键入help print并且往下拖拽,你会看到:
‘print‘ is an abbreviation for ‘expression --‘. 
打印对象
如果我们尝试键入
p objects 
那输出会有点冗繁:
(NSString *) $7 = 0x0000000104da4040 @"red balloons" 
当尝试打印一个更加复杂的数据结构时候会情况会更糟:
1. (lldb) p @[ @"foo", @"bar" ]
2.  
3. (NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"
好吧,我们想看下对象的description方法。我们需要告诉expression命令作为对象来打印这个结果,使用-O标志(这不是0):
1. (lldb) e -O -- $8
2. <__NSArrayI 0x7fdb9b71b3e0>(
3. foo,
4. bar
5. )
很走运,e -O --也有别名,其别名为po,我们可以只要这样使用:
1. (lldb) po $8
2. <__NSArrayI 0x7fdb9b71b3e0>(
3. foo,
4. bar
5. )
6. (lldb) po @"lunar"
7. lunar
8. (lldb) p @"lunar"
9. (NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"
打印变量
print命令有许多种不同的格式可以由你来指定。它们以命令格式为print/<fmt>或者更简单p/<fmt>。接下来举个栗子。
默认的格式:
1. (lldb) p 16
2. 16
16进制格式:
1. (lldb) p/x 16
2. 0x10
二进制格式(t代表tow):
1. (lldb) p/t 16
2. 0b00000000000000000000000000010000
3. (lldb) p/t (char)16
4. 0b00010000
你还可以使用p/c打印字符,或者是p/s打印一个非终止类型的字符串char *。完整列表戳这里。
变量
至此你可以打印对象跟简单得类型,并可以在调试器中使用expression命令更改它们的值,让我们使用一些变量来减少我们输入工作。你可以声明一个变量C来表示int a = 0,同样你可以在LLDB中做同样的事情。然后,变量必须以美元符号作为开头:
1. (lldb) e int $a = 2
2. (lldb) p $a * 19
3. 38
4. (lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ]
5. (lldb) p [$array count]
6. 2
7. (lldb) po [[$array objectAtIndex:0] uppercaseString]
8. SATURDAY
9. (lldb) p [[$array objectAtIndex:$a] characterAtIndex:0]
10. error: no known method ‘-characterAtIndex:‘; cast the message send to the method‘s return type
11. error: 1 errors parsing expression
噢。LLDB不能识别出所牵扯的变量类型。不时会遇到,我们可以给一点提示:
1. (lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]
2. ‘M‘
3. (lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]
4. 77
变量特性让调试器更容易被使用,你这么认为吗?
流程控制
你的程序会在你打上断点的位置停下来。
此时你看到在调试工具栏有四个按钮,通过使用它们你可以控制程序的执行流程:
 
这四个按钮从左到右依次为:继续,单步,跳入,跳出。
首先,继续按钮将会让你得程序继续正常执行(可能一直运行或者遇到下一个断点)。在LLDB中,你可以使用process continue来继续执行,别名为c。
其次,单步执行将会将单行代码当做黑盒一样执行。如果那行你调用了函数,那将不会进入这个函数,而是直接执行这个函数后继续运行。LLDB中相对应的命令是thread step-over,next,或者 n。
如果你想进入一个函数调用来检查调试该函数的执行,你可以使用第三个按钮,跳入,LLDB同样提供了thread step-in,step, 和s。注意到next与step在当前行代码不涉及函数调用的时候效果是一样的。
大部分知道c,n,s。但是还有第四个按钮,跳出。如果你不小心跳入了一个函数而你本意是想跳过它,一般反应是不断的按n知道函数返回。跳出帮你节省时间。它会执行到return语句(知道执行了出栈操作),然后会停下来。
举个栗子
来看下如下的代码片段:
 
代码停在断点,然后我们执行如下的命令行:
1. p i
2. n
3. s
4. p i
5. finish
6. p i
7. frame info
这里,frame info将会告诉你当前行以及源文件是啥,可以通过键入help frame,help thread,以及help process获取更多信息。那么输出什么呢?先思考之前的描述想下答案!
1. (lldb) p i
2. (int) $0 = 99
3. (lldb) n
4. 2014-11-22 10:49:26.445 DebuggerDance[60182:4832768] 101 is odd!
5. (lldb) s
6. (lldb) p i
7. (int) $2 = 110
8. (lldb) finish
9. 2014-11-22 10:49:35.978 DebuggerDance[60182:4832768] 110 is even!
10. (lldb) p i
11. (int) $4 = 99
12. (lldb) frame info
13. frame #0: 0x000000010a53bcd4 DebuggerDance`main + 68 at main.m:17
仍在17行的原因是finish命令会让程序运行直到isEven()函数返回,然后马上停止。但是请注意,17行已经执行完了。
线程返回
还有一个特别帮的功能是你在调试的时候可以用thread return来控制程序流程。它使用可选参数,将这个参数载入寄存器,单后马上执行返回命令,然后函数出栈。这意味着剩下函数没有被执行。这样因为ARC的引用计数/记录出现问题,或者遗漏一些清除操作。但在一个函数的开头执行这个命令是一个非常棒得函数打桩并且反悔了一个伪结果。
让我们来对上述相同的代码段跑如下的指令:
1. p i
2. s
3. thread return NO
4. n
5. p even0
6. frame info
在看答案之前乡下结果,答案如下:
1. (lldb) p i
2. (int) $0 = 99
3. (lldb) s
4. (lldb) thread return NO
5. (lldb) n
6. (lldb) p even0
7. (BOOL) $2 = NO
8. (lldb) frame info
9. frame #0: 0x00000001009a5cc4 DebuggerDance`main + 52 at main.m:17
断点
我们一直都使用断点来让程序停止,检视当前状态从而捕获BUG。但是如果我们转变对断点的理解,我们可以获得更多可能。
A breakpoint allows you to instruct a program when to stop, and then allows the running of commands.
考虑在函数刚开始处打一个断点,使用thread return来重写函数行为,然后继续。现在想象下自动实现这种处理。是不是听起来很牛X,不是么?
断点管理
Xcode提供了一套工具来创建和操作断点。我们将会逐一过一遍并且进行描述与之对应的LLDB命令行。
在Xcode的左面板上,有一堆按钮集合。有一个长得很像断点。点击打开断点导航栏,进去之后你一眼看到你所操作的所有断点:
 
这里你可以看到所有的断点 - 对应LLDB中的breakpoint list或者是br li。你可以点击单个断点进行打开或者关闭 - 对应LLDB中的breakpoint enable <breakpointID>和breakpoint disable <breakpointID>:
1. (lldb) br li
2. Current breakpoints:
3. 1: file = ‘/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m‘, line = 16, locations = 1, resolved = 1, hit count = 1
4.  
5.   1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, resolved, hit count = 1
6.  
7. (lldb) br dis 1
8. 1 breakpoints disabled.
9. (lldb) br li
10. Current breakpoints:
11. 1: file = ‘/Users/arig/Desktop/DebuggerDance/DebuggerDance/main.m‘, line = 16, locations = 1 Options: disabled
12.
13.   1.1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab, unresolved, hit count = 1
14.  
15. (lldb) br del 1
16. 1 breakpoints deleted; 0 breakpoint locations disabled.
17. (lldb) br li
18. No breakpoints currently set.
创建断点
(UI创建略了。。。是人都会吧。。)
在调试器中打断点,使用breakpoint set命令:
1. (lldb) breakpoint set -f main.m -l 16
2. Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x
缩写可以用br。b是另外一个完全不同的命令,是_regexp-break的别名,但是它足够健壮来进行创建上述命令一样效果的断点:
1. (lldb) b main.m:17
2. Breakpoint 2: where = DebuggerDance`main + 52 at main.m:17, address = 0x
你也可以防止一个断点在一个符号(C语言函数),而不用指定行数:
1. (lldb) b isEven
2. Breakpoint 3: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
3. (lldb) br s -F isEven
4. Breakpoint 4: where = DebuggerDance`isEven + 16 at main.m:4, address
5.
现在这些断点会停止正在将要执行的函数,同样适用与OC方法:
1. (lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
2. Breakpoint 5: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
3. (lldb) b -[NSArray objectAtIndex:]
4. Breakpoint 6: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950
5. (lldb) breakpoint set -F "+[NSSet setWithObject:]"
6. Breakpoint 7: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820
7. (lldb) b +[NSSet setWithObject:]
8. Breakpoint 8: where = CoreFoundation`+[NSSet setWithObject:], address = 0x000000010abd3820
如果你想通过UI来创建象征性断点,你可以点击左下端断点导航栏的+号:
 
然后选择第三个选项:
 
此时出现弹出框让你输入比如-[NSArray objectAtIndex:]的符号,然后程序在这个函数调用的时候便可以停止下来,不管是你的代码或者还是大苹果的代码!
如果我们看下其他选项,我们可以发现一些有意思的选项,同样提供了各种条件触发的锻炼只要你点击了Xcode的UI并且选择了“Edit Breakpoint”选项:
 
如上图,断点只有在i为99的时候才会停止程序。你可以同样设置“ ignore”选项来告诉断点在前n次调用的时候不用停止程序(条件为真)。
这里还有一个“Add Action”按钮。。。
断点动作
可能上面断点的栗子中,你想知道每次断点时候i值是多少。我们可以使用动作p i,然后当断点触发的时候我们进入调试器,它会预先执行这个命令在将控制流程交给你之前:
 
你也可以加多重动作,可以是调试器指令,shell指令或者更健壮的打印信息:
 
如上你可以看到打印出i值,还有强调语句,打印出自定义的表达式。
下面是上述功能用纯LLDB命令代替Xcode的UI:
1. (lldb) breakpoint set -F isEven
2. Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00
3. (lldb) breakpoint modify -c ‘i == 99‘ 1
4. (lldb) breakpoint command add 1
5. Enter your debugger command(s).  Type ‘DONE‘ to end.
6. > p i
7. > DONE
8. (lldb) br li 1
9. 1: name = ‘isEven‘, locations = 1, resolved = 1, hit count = 0
10.     Breakpoint commands:
11.       p i
12.
13. Condition: i == 99
14.  
15.   1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0
自动化,我们来了!
计算值之后继续
如果视线停留在断点弹出框的底端,你会额外看到一个选项:“Automatically continue after evaluation actions(计算动作后自动执行)。”它只是一个勾选框,但是它却有强大的能力。如果你勾选上了,调试器将会苹果你所有的命令然后继续执行程序。表面上看上跟断点没有打住一样(除非你断点太多了,拖慢了程序进度)。
这个勾选框功能与最后一个动作断点继续执行效果一样,但是有勾选框更加容易点。对应调试器的指令如下:
1. (lldb) breakpoint set -F isEven
2. Breakpoint 1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00
3. (lldb) breakpoint command add 1
4. Enter your debugger command(s).  Type ‘DONE‘ to end.
5. > continue
6. > DONE
7. (lldb) br li 1
8. 1: name = ‘isEven‘, locations = 1, resolved = 1, hit count = 0
9.     Breakpoint commands:
10.       continue
11.  
12.   1.1: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x00000001083b5d00, resolved, hit count = 0
计算后自动继续运行让你可以单独通过使用断点来修改你的程序!你可以停止在单行,运行一个expression命令来改变变量,然后继续。
举个栗子
考虑下简陋残酷的“打印式调试”技术。不是用:
NSLog(@"%@", whatIsInsideThisThing); 
而是用断点处设置打印变量值替代吊打印日志打印语句然后继续。
不是用:
1.   return 9;
2.   /*
3.    ...
4. }
5.
而是用断点处调用thread return 9然后继续执行。
带动作的象征断点确实真的很强大。你也可以添加这些断点到你朋友的Xcode工程并且让动作将所有信息细致展示出来。接下来看看要耗时多久来进行计算以及会发生什么吧。
调试器完整操作
在起舞之前还有一点需要我们注意。你真的可以在调试器中执行任何的C/OC/C++/Swift命令。比较弱的是我们不能创建一个新的函数。。。这意味着没有新的类,块,函数,带虚方法的C++类等等。除了这个,调试器什么都能满足!
我们可以分配一些字节:
1. (lldb) e char *$str = (char *)malloc(8)
2. (lldb) e (void)strcpy($str, "munkeys")
3. (lldb) e $str[1] = ‘o‘
4. (char) $0 = ‘o‘
5. (lldb) p $str
6. (char *) $str = 0x00007fd04a900040 "monkeys"
或者我们可以检查一些内存(使用x命令)来看我们新数组的4个字节:
1. (lldb) x/4c $str
2. 0x7fd04a900040: monk
我们还可以后三个字节:
1. (lldb) x/1w `$str + 3`
2. 0x7fd04a900043: keys
当你所要的活结束的时候别忘记了释放内存避免造成内存泄露:
(lldb) e (void)free($str) 
跳舞吧,骚年!
现在我们已经清楚基础步骤,是时候来整一些比较疯狂的东西了。我过去曾写过一篇博客(大家自己收藏。。。)发表在looking at the internals of NSArray。当时用了大量的NSLog语句,后来全用调试器搞定了。它是一个很好的调试器使用练习。
畅通无阻(无断点模式)
当你的应用在跑的时候,Xcode中的调试工具栏展示一个停止按钮而非继续状态的按钮:
 
选中这个按钮的时候,应用遇到断点将会停止(就像输入了process interrupt)。这时候将会让你进入调试器。
这里有一个有趣的地方。如果你运行一个iOS应用,你可以尝试这个(全局变量可提供)
1. (lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
2. <UIWindow: 0x7f82b1fa8140; frame = (0 0; 320 568); gestureRecognizers = <NSArray: 0x7f82b1fa92d0>; layer = <UIWindowLayer: 0x7f82b1fa8400>>
3.    | <UIView: 0x7f82b1d01fd0; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x7f82b1e2e0a0>>
可以看到整个层级!Chisel(上文提及)用pviews来实现。
更新UI
然后,通过上述的输出,我们可以看到隐藏的视图:
(lldb) e id $myView = (id)0x7f82b1d01fd0 
然后在调试器中修改它的背景色:
(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]] 
在你下次继续运行这个程序的时候你才会看到变化。这因为这个变化需要传递给渲染服务然后视图展示才会被更新。
渲染服务实际上是另一个进程(称作后台),并且甚至我们调试进程被停止了,这个后台也不会被停止!
这意味着不通过继续,你可以执行:
(lldb) e (void)[CATransaction flush] 
在模拟器中或者设备中的UI会进行刷新而你还在调试器中!Chisel提供了一个别名函数叫做caflush,并且它被用来实现其它捷径像hide <view>,show <view>还有其他许多许多。所有的Chisel命令都有对应的文档,所以就在安装它之后键入help来随心所欲的获取更多的信息吧。
压入视图控制器
想象一个简单的应用有一个UINavigationController作为根视图控制器。你可以在调试器中相当简易的执行如下操作:
(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController] 
然后压入子视图控制器:
1. (lldb) e id $vc = [UIViewController new]
2. (lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]
3. (lldb) e (void)[$vc setTitle:@"Yay!"]
4. (lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]
最后执行:
(lldb) caflush // e (void)[CATransaction flush] 
你会看到马上压入了一个视图控制器。
找到按钮的目标
想象下你调试器中有一个变量,$myButton,你想要去创建它,并从UI中抓取它,或者简单地只是你想在断点停下来的时候将它作为个局部变量。你可能想知道当你点击它的时候是谁接收了这个动作。这里展示达到这点有多么的简单:
1. (lldb) po [$myButton allTargets]
2. {(
3.     <MagicEventListener: 0x7fb58bd2e240>
4. )}
5. (lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
6. <__NSArrayM 0x7fb58bd2aa40>(
7. _handleTap:
8. )
现在你可能想在事件发生的时候添加一个断点。只要在LLDB或者Xcode设置象征性断点在-[MyEventListener _handleTap:]。and you are all set to Go!
观察实例变量值变化
想象一个假设的场景你有一个UIView且它的_layer实例变量被重写了。因为这里可能不涉及方法,我们不能使用象征性断点。取而代之的是我们想观察一个内存地址什么时候被写入了。
首先我们需要找到_layer对象在那里:
1. (ptrdiff_t) $0 = 8
2.
现在我们知道($myView + 8)这个内存地址被写入了:
1. (lldb) watchpoint set expression -- (int *)$myView + 8
2. Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
3.     new value: 0x0000000000000000
对应Chisel里面的wivar $myView _layer。
在非重写方法上的象征性断点
想象你想知道什么时候-[MyViewController viewDidAppear:]被调用了。如果MyViewController实际上没有实现这个方法,但是父类实现了呢?我们可以设置一个断点来看看具体情况:
1. (lldb) b -[MyViewController viewDidAppear:]
2. Breakpoint 1: no locations (pending).
3. WARNING:  Unable to resolve breakpoint to any actual locations.
因为LLDB根据符号搜索,它找不到该方法,所以你的断点将不会被触发。你所需要做的是设置一个条件,[self isKindofClass:[MyViewController class]],然后见这个断点设在UIViewController上。一般来说,设置一个这样的条件是有效的,但是,这里无效是因为我们没有父类该方法的实现。
viewDidAppear:是大苹果写的,所以没有对应的符号;在方法内部也没有self。如果你想要使用在象征性断点内使用self,你需要知道它在那里(可能在寄存器也可能在栈上;在x86你可能在$esp+4找到它)。这是个通过的历程,因为你知道已经知道有四种体系架构了。吐槽略。。幸运的是,Chisel已经完成了这些封装,你可以调用bmessage:
1. (lldb) bmessage -[MyViewController viewDidAppear:]
2. Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28
3. Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c
LLDB与Python
LLDB有完整的内置Python支持。如果你在LLDB上输入脚本,它会打开一个Python REPL。如果你在LLDB中键入script,它会打开一个Python REPL。你可以传入一行Python语句到script命令来不进入REPL的情况下进行执行脚本:
1. (lldb) script import os
2. (lldb) script os.system("open http://www.objc.io/")
这允许你创建各种各样的酷比命令。将这个丢入文件,~/myCommands.py:
1.   debugger.HandleCommand("e (void)[CATransaction flush]")
2.
然后在LLDB中运行如下:
command script import ~/myCommands.py 
或者,将这行代码放置于/.lldbinit让LLDB每次运行的时候都执行一次。Chisel不过就是一堆Python脚本用来组合字符串,然后告诉LLDB来执行这些字符串。听起来很简单吧!呃?
参考链接:
1.当异常出现时
2.日志记录CocoaLumberjack
3.在Xcode中调试程序
4.南峰子的技术博客
5.与调试器共舞 - LLDB 的华尔兹
6.官方调试技巧文档
7.inspecting-obj-c-parameters-in-gdb

以上是关于[转] Xcode 高级调试技巧的主要内容,如果未能解决你的问题,请参考以下文章

Xcode 调试技巧-b

Xcode调试小技巧

XCode基本使用及调试技巧

iOS开发之Xcode常用调试技巧总结

Xcode 常用调试技巧总结

idea 高级调试技巧