xcode各种调试手段
有6种方法,分别是:
NO1:最常见,也就是NSLog
从NSLog打印出的任何东西都会变成代码,任何人都可以看见。将设备连接到电脑,打开XCode中的organizer,就可以从console查看到每条日志信息,这会带来很大的影响。想一下,你想把一些保密的算法逻辑或者用户密码打印到console。正因为这个,如果苹果发现在项目发布中,有太多内容输出到console,那么你的应用可能会遭到苹果的拒绝。
NO2:Enable NSZombie Objects (开启僵尸对象)--EXC_BAD_ACCESS
NSZombieEnabled变量用来调试与内存有关的问题,跟踪对象的释放过程。启用了NSZombieEnabled的话,它会用一个僵尸来替换默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现会将该对象转换成僵尸对象。僵尸对象的作用是在你向它发送消息时,它会显示一段日志并自动跳入调试器。
所以,当在应用中启用NSZombie而不是让应用直接崩溃掉时,一个错误的内存访问就会变成一条无法识别的消息发送给僵尸对象。僵尸对象会显示接受到得信息,然后跳入调试器,这样你就可以查看到底是哪里出了问题。
可以在Xcode的scheme页面中设置NSZombieEnabled环境变量。点击Product——>Edit Scheme打开该页面,然后勾选Enable Zombie Objects 复选框。

然后选中下图中的选项

Zombie模式不能再真机上使用,只能在模拟器上使用
NO3:断点调试(全局断点、条件断点、符号断点)
一、全局断点(异常断点):异常断点可以快速定位不满足特定条件的异常,比如常见的数组越界,这时候很难通过异常信息定位到错误所在位置。这个时候异常断点就可以发挥作用了。
NSArray *aa = @[@2,@4]; NSLog(@"%@",aa[3]);
这两行代码,没有添加全局断点时,运行crash,直接就跳到了mian函数,如下图:

接下来添加全局断点,方法如下图:

点击了Exception BreakPoint 后

添加之后运行,奔溃后,程序停留在了奔溃的那行代码。

Exception:可以选择抛出异常对象类型:OC或C++。
Break:选择断点接收的抛出异常来源是Throw还是Catch语句。
ps 不过有时候程序奔溃,这种方式定位不到
二、条件断点:设置断点触发的条件,方便开发者对特定情况进行调试
添加(删除)条件断点的快捷键:command+\
如下图:
在for循环中添加一个断点。右击断点选择”Edit BreakPoint”,然后设置断点触发条件。

这个例子当 “i==5”时,断点触发,如下图:

三、符号断点:

选择Symbolic Breakpoint后
例如:unrecognized selector send to instancd 的快速定位
NO4:
设置完成后再遇到类似的错误就会定位到具体的代码
NO4:Static Analyzer (静态分析)
Static Analyzer主要用于分析内存,避免内存泄漏。主要对以下情况进行分析。
未使用的实例变量、未初始化的实例变量、类型不兼容、无法达到的路径、引用空指针
使用快捷键:command + shift +B,可以去如下图中去设置

如下图就能轻松找到可能内存泄漏的代码,然后我们根据代码环境进行修复就可以了

(ps:有的内存泄漏可能检测不出来,还是需要我们在写代码时对内存这块多留点心。)
NO5:Address Sanitizer
在Xcode7之后新增了AddressSanitizer工具,为我们调试EXC_BAD_ACCESS错误提供了便利。当程序创建变量分配一段内存时,将此内存后面的一段内存也冻结住,标识为中毒内存。程序访问到中毒内存时(访问越界),立即中断程序,抛出异常并打印异常信息。你可以根据中断位置及输出的Log信息来解决错误。当然,如果变量已经释放了,它所占用的内存也会被标识为中毒内存,这个时候访问这片内存空间同样会抛出异常。
使用方法:
“Edit Scheme…” —> “Run” —> “Diagnostics” —> “Zombie Objects”

开启AddressSanitizer之后,在调试程序的过程中,如果有遇到EXC_BAD_ACCESS错误,程序则会自动终止,抛出异常。
NO6:LLDB调试器
LLDB是一个有着 REPL 的特性和 C++ ,Python 插件的开源调试器。LLDB 绑定在 Xcode 内部,存在于主窗口底部的控制台中。调试器允许你在程序运行的特定时暂停它,你可以查看变量的值,执行自定的指令,并且按照你所认为合适的步骤来操作程序的进展。(这里有一个关于调试器如何工作的总体的解释。)
你以前有可能已经使用过调试器,即使只是在 Xcode 的界面上加一些断点。但是通过一些小的技巧,你就可以做一些非常酷的事情。GDB to LLDB 参考是一个非常好的调试器可用命令的总览。你也可以安装 Chisel,它是一个开源的 LLDB 插件合辑,这会使调试变得更加有趣。
与此同时,让我们以在调试器中打印变量来开始我们的旅程吧。
基础
这里有一个简单的小程序,它会打印一个字符串。注意断点已经被加在第 8 行。断点可以通过点击 Xcode 的源码窗口的侧边槽进行创建。

程序会在这一行停止运行,并且控制台会被打开,允许我们和调试器交互。那我们应该打些什么呢?
help
最简单命令是 help,它会列举出所有的命令。如果你忘记了一个命令是做什么的,或者想知道更多的话,你可以通过 help来了解更多细节,例如 help print 或者 help thread。如果你甚至忘记了 help 命令是做什么的,你可以试试 help help。不过你如果知道这么做,那就说明你大概还没有忘光这个命令。??
打印值很简单;只要试试 print 命令:

LLDB 实际上会作前缀匹配。所以你也可以使用 prin,pri,或者 p。但你不能使用 pr,因为 LLDB 不能消除和 process 的歧义 (幸运的是 p 并没有歧义)。
你可能还注意到了,结果中有个 $0。实际上你可以使用它来指向这个结果。试试 print $0 + 7,你会看到 106。任何以美元符开头的东西都是存在于 LLDB 的命名空间的,它们是为了帮助你进行调试而存在的。
expression
如果想改变一个值怎么办?你或许会猜 modify。其实这时候我们要用到的是 expression 这个方便的命令。
这不仅会改变调试器中的值,实际上它改变了程序中的值。这时候继续执行程序,将会打印 42 red balloons。神奇吧。
注意,从现在开始,我们将会偷懒分别以 p 和 e 来代替 print 和 expression。
什么是 print 命令
考虑一个有意思的表达式:p count = 18。如果我们运行这条命令,然后打印 count 的内容。我们将看到它的结果与 expression count = 18 一样。
和 expression 不同的是,print 命令不需要参数。比如 e -h +17 中,你很难区分到底是以 -h 为标识,仅仅执行 +17 呢,还是要计算 17 和 h 的差值。连字符号确实很让人困惑,你或许得不到自己想要的结果。
幸运的是,解决方案很简单。用 -- 来表征标识的结束,以及输入的开始。如果想要 -h 作为标识,就用 e -h -- +17,如果想计算它们的差值,就使用 e -- -h +17。因为一般来说不使用标识的情况比较多,所以 e -- 就有了一个简写的方式,那就是 print。
输入 help print,然后向下滚动,你会发现:
|
1
2
|
'print' is an abbreviation for 'expression --'. (print是 `expression --` 的缩写) |
打印对象
尝试输入
|
1
|
p objects |
输出会有点啰嗦
|
1
|
(NSString *) $7 = 0x0000000104da4040 @"red balloons" |
如果我们尝试打印结构更复杂的对象,结果甚至会更糟
|
1
2
|
(lldb) p @[ @"foo", @"bar" ](NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects" |
实际上,我们想看的是对象的 description 方法的结果。我么需要使用 -O (字母 O,而不是数字 0) 标志告诉 expression 命令以 对象 (Object) 的方式来打印结果。
|
1
2
3
4
|
(lldb) e -O -- $8(foo,bar) |
幸运的是,e -o -- 有也有个别名,那就是 po (print object 的缩写),我们可以使用它来进行简化:
|
1
2
3
4
5
6
7
8
|
(lldb) po $8(foo,bar)(lldb) po @"lunar"lunar(lldb) p @"lunar"(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar" |
打印变量
可以给 print 指定不同的打印格式。它们都是以 print/或者简化的 p/格式书写。下面是一些例子:
默认的格式
|
1
2
|
(lldb) p 1616 |
十六进制:
|
1
2
|
(lldb) p/x 160x10 |
二进制 (t 代表 two):
|
1
2
3
4
|
(lldb) p/t 160b00000000000000000000000000010000(lldb) p/t (char)160b00010000 |
你也可以使用 p/c 打印字符,或者 p/s 打印以空终止的字符串 (译者注:以 '\0' 结尾的字符串)。
这里是格式的完整清单。
变量
现在你已经可以打印对象和简单类型,并且知道如何使用 expression 命令在调试器中修改它们了。现在让我们使用一些变量来减少输入量。就像你可以在 C 语言中用 int a = 0 来声明一个变量一样,你也可以在 LLDB 中做同样的事情。不过为了能使用声明的变量,变量必须以美元符开头。
|
1
2
3
4
5
6
7
8
9
10
11
|
(lldb) e int $a = 2(lldb) p $a * 1938(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ](lldb) p [$array count]2(lldb) po [[$array objectAtIndex:0] uppercaseString]SATURDAY(lldb) p [[$array objectAtIndex:$a] characterAtIndex:0]error: no known method '-characterAtIndex:'; cast the message send to the method's return typeerror: 1 errors parsing expression |
悲剧了,LLDB 无法确定涉及的类型 (译者注:返回的类型)。这种事情常常发生,给个说明就好了:
|
1
2
3
4
|
(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]'M'(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]77 |
变量使调试器变的容易使用得多,想不到吧???
bt
打印调用堆栈信息,使用 bt all命令可以打印出所有thread的调用栈信息
比如说:call [self.view setBackgroundColor:[UIColor redColor]],使用这个命令来设置view controller的背景色为红色
对中文的兼容
有中文时必须使用 [NSString stringWithUTF8String:] 方法,原因在于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
类型的问题
出现类型不确定或类型不匹配的时候,就会报错,这个时候必须强制转换才可以。
比如前文中获取indexPath的row的方法的表达式:(int)[indexPath row],或者类似下面这种干脆无返回值 p (void)NSLog(@"%@",[self.view viewWithTag:1001]),或者上文中的 p (char)[[$array objectAtIndex:$a] characterAtIndex:0],这些场景中都需要明确输出的类型。
找不到方法
注意,使用系统框架对象的属性时不能使用dot语法,比如下图中的问题。

改成如下的格式才行:

流程控制
当你通过 Xcode 的源码编辑器的侧边槽 (或者通过下面的方法) 插入一个断点,程序到达断点时会就会停止运行。
调试条上会出现四个你可以用来控制程序的执行流程的按钮。

从左到右,四个按钮分别是:continue,step over,step into,step out。
第一个,continue 按钮,会取消程序的暂停,允许程序正常执行 (要么一直执行下去,要么到达下一个断点)。在 LLDB 中,你可以使用 process continue 命令来达到同样的效果,它的别名为 continue,或者也可以缩写为 c。
第二个,step over 按钮,会以黑盒的方式执行一行代码。如果所在这行代码是一个函数调用,那么就不会跳进这个函数,而是会执行这个函数,然后继续。LLDB 则可以使用 thread step-over,next,或者 n 命令。
如果你确实想跳进一个函数调用来调试或者检查程序的执行情况,那就用第三个按钮,step in,或者在LLDB中使用 thread step in,step,或者 s 命令。注意,当前行不是函数调用时,next 和 step 效果是一样的。
大多数人知道 c,n 和 s,但是其实还有第四个按钮,step out。如果你曾经不小心跳进一个函数,但实际上你想跳过它,常见的反应是重复的运行 n 直到函数返回。其实这种情况,step out 按钮是你的救世主。它会继续执行到下一个返回语句 (直到一个堆栈帧结束) 然后再次停止。
例子
考虑下面一段程序:

假如我们运行程序,让它停止在断点,然后执行下面一些列命令:
|
1
2
3
4
5
6
7
|
p insp ifinishp iframe info |
这里,frame info 会告诉你当前的行数和源码文件,以及其他一些信息;查看 help frame,help thread 和 help process 来获得更多信息。这一串命令的结果会是什么?看答案之前请先想一想。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
(lldb) p i(int) $0 = 99(lldb) n2014-11-22 10:49:26.445 DebuggerDance[60182:4832768] 101 is odd!(lldb) s(lldb) p i(int) $2 = 110(lldb) finish2014-11-22 10:49:35.978 DebuggerDance[60182:4832768] 110 is even!(lldb) p i(int) $4 = 99(lldb) frame infoframe #0: 0x000000010a53bcd4 DebuggerDance`main + 68 at main.m:17 |
它始终在 17 行的原因是 finish 命令一直运行到 isEven() 函数的 return,然后立刻停止。注意即使它还在 17 行,其实这行已经被执行过了。
Thread Return
调试时,还有一个很棒的函数可以用来控制程序流程:thread return 。它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。
让我们稍微修改一下上面代码段并运行:
|
1
2
3
4
5
6
|
p isthread return NOnp even0frame info |
看答案前思考一下。下面是答案:
|
1
2
3
4
5
6
7
8
9
|
(lldb) p i(int) $0 = 99(lldb) s(lldb) thread return NO(lldb) n(lldb) p even0(BOOL) $2 = NO(lldb) frame infoframe #0: 0x00000001009a5cc4 DebuggerDance`main + 52 at main.m:17 |
Chisel-LLDB命令插件
Chisel介绍
源码地址:Chisel
Chisel扩展了一些列的lldb的命令来帮助iOS开发者调试iOS应用程序。
安装Chisel
1.确保终端安装了Homebrew
2.终端执行命令:brew install chisel 输入命令后我遇到第一个问题。

碰见这个问题终端执行命令:sudo chown -R ${USER} /Library/Caches/Homebrew/,执行此命令后问题解决。
3.如果没有第二步的问题,执行命令:brew install chisel后出现如下界面

4.注意看Caveats下面的那两行,意思是把第二行的文字command script import /usr/local/opt/chisel/libexec/fblldb.py添加到.lldbinit文件中,这时执行命令echo command script import /usr/local/opt/chisel/libexec/fblldb.py >> ~/.lldbinit(粗体文字替换为你终端Caveats下面的第二行文字)可免去你去找.lldbinit文件,或者.lldbinit文件不出现的烦恼啊。到此步不出意外已经安装成功。
5.安装成功后重新启动Xcode即可。
6.终端下检查是否安装成功输入命令:lldb,然后输入help,往下翻出现如下界面为成功

7.Xcode下检查是否安装成功,打断点进入lldb下输入help,往下翻出现如下界面为成功

2.内置命令
Chisel 为lldb提供了新增的便捷命令,是非常实用的命令
2.1 pviews
这个命令可以递归打印所有的view,并能标示层级,相当于 UIView 的私有辅助方法 [view recursiveDescription] 。 善用使用这个功能会让你在调试定位问题时省去很多麻烦。
使用示例:
(lldb) pviews view <TestView: 0x18df8070; baseClass = UIControl; frame = (144 9; 126 167); layer = <CALayer: 0x18df8150>> | <UIView: 0x18df81d0; frame = (0 0; 126 126); userInteractionEnabled = NO; layer = <CALayer: 0x18df8240>> | <UIImageView: 0x18df8330; frame = (0 0; 126 126); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x18df83b0>> | <UILabel: 0x18df8460; frame = (0 135; 126 14); text = 'haha'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x18df7fb0>> | | <_UILabelContentLayer: 0x131a3d50> (layer) | <UILabel: 0x18df8670; frame = (0 155; 126 12); text = 'hahaha'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x18df8730>> | | <_UILabelContentLayer: 0x131bea10> (layer) | <UIImageView: 0x18df88d0; frame = (0 9; 28 27); hidden = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x18df8ba0>>
2.2 pvc
这个命令也是递归打印层级,但是不是view,而是viewController。利用它我们可以对viewController的结构一目了然。 其实苹果在IOS8也默默的添加了 UIViewController 的一个私有辅助方法 [UIViewController _printHierarchy] 同样的效果。
预览效果:
(lldb) pvc <TabBarController: 0x13772fd0; view = <UILayoutContainerView; 0x151b3a30>; frame = (0, 0; 414, 736)> | <UINavigationController: 0x1602b800; view = <UILayoutContainerView; 0x1b00aca0>; frame = (0, 0; 414, 736)> | | <FirstViewController: 0x16029c00; view = <UIView; 0x1b01e1c0>; frame = (0, 0; 414, 736)> | <UINavigationController: 0x138c5200; view = <UILayoutContainerView; 0x1316a080>; frame = (0, 0; 414, 736)> | | <SecondViewController: 0x16030400; view = <UIView; 0x2094b370>; frame = (0, 0; 414, 736)> | | | <SecondChildViewController: 0x15af6000; view = <UIView; 0x18d4e650>; frame = (0, 64; 414, 628)> | <UINavigationController: 0x1383ca00; view = <UILayoutContainerView; 0x13180070>; frame = (0, 0; 414, 736)> | | <ThirdViewController: 0x138ddc00; view = <UIView; 0x18df6650>; frame = (0, 0; 414, 736)> | | | <ThirdChild1ViewController: 0x1393fe00; view = <UIView; 0x131ec000>; frame = (0, 0; 414, 672)> | | | <ThirdChild2ViewController: 0x138dce00; view = <UIView; 0x204075a0>; frame = (414, 0; 414, 672)> | | | <ThirdChild3ViewController: 0x138a8e00; view = <UIView; 0x20426250>; frame = (828, 0; 414, 672)> | <UINavigationController: 0x160eca00; view = <UILayoutContainerView; 0x152f7d90>; frame = (0, 0; 414, 736)> | | <FourViewController: 0x13157cc0; view not loaded>
是不是方便很多呢,而且还可以看到 viewController 是否已经 viewDidLoad .
2.3 visualize
这是个很有意思的功能,它可以让你使用Mac的预览打开一个 UIImage, CGImageRef, UIView, 或 CALayer。 这个功能或许可以帮我们用来截图、用来定位一个view的具体内容。 但是在我试用了一下,发现暂时还是只能在模拟器时使用,真机还不行。
使用简单:
(lldb) visualize imageView
2.4 fv & fvc
fv 和 fvc 这两个命令是用来通过类名搜索当前内存中存在的view和viewController实例的命令,支持正则搜索。
如:
(lldb) fv scrollView 0x18d3b8c0 UIScrollView 0x137d0c50 UIScrollView 0x131b1580 UIScrollView 0x131b2070 UIScrollView (lldb) fvc Home 0x1393fe00 HomeFeedsViewController 0x138a8e00 HomeFeedsViewController (lldb)
2.5 show & hide
这两个命令用来显示和隐藏一个指定的 UIView .
(lldb) show self.view (lldb) hide self.view
也可以使用内存地址隐藏和现实view,比如通过 fv cate找到一个view后使用hide隐藏它
(lldb)fv cate
0x7fd5b6e06920 AlbumCategoryView
(lldb) hide 0x7fd5b6e06920
2.6 mask/umask border/unborder
这两组命令用来标识一个view或layer的位置时用, mask用来在view上覆盖一个半透明的矩形, border可以给view添加边框。但是在我实际使用的过程中mask总是会报错,估计是有bug, 那么mask/unmask 一般不要用好了,用border命令是一样的效果,反正二者的用途都是找到一个对应的view.
如果我要给第二个view添加一个颜色为蓝色,宽度为2的边框,之后再用unborder命令移除,操作如下:

通过help border命令知道border的使用格式如下:
Options: --color/-c <color>; Type: string; A color name such as 'red', 'green', 'magenta', etc. --width/-w <width>; Type: CGFloat; Desired width of border. Syntax: border [--color=color] [--width=width] <viewOrLayer>
其中viewOrLayer表示你要修改的view的地址,我们通过pviews命令知道,第二个view的地址是0x7feae2d605f0,所以我们输入
border -c blue -w 2 0x7feae2d605f0 //添加边框 unborder 0x7feae2d605f0 //移除边框
注意我在输入每个border/unborder命令时,右侧模拟器第二个view(红色view)的变化。
2.7 caflush
这个命令会重新渲染,即可以重新绘制界面, 相当于执行了 [CATransaction flush] 方法,要注意如果在动画过程中执行这个命令,就直接渲染出动画结束的效果。
当你想在调试界面颜色、坐标之类的时候,可以直接在控制台修改属性,然后caflush就可以看到效果啦,是不是要比改代码,然后重新build省事多了呢。
例, 其中 $122 即是目标UIView:
(lldb) p view (long) $122 = 140718754142192 (lldb) e (void)[$122 setBackgroundColor:[UIColor greenColor]] (lldb) caflush
2.8 bmessage
这个命令就是用来打断点用的了,虽然大家断点可能都喜欢在图形界面里面打,但是考虑一种情况:我们想在 [MyViewController viewWillAppear:] 里面打断点,但是 MyViewController并没有实现 viewWillAppear: 方法, 以往的作法可能就是在子类中实现下viewWillAppear:,然后打断点,然后rebuild。
那么幸好有了 bmessage命令。我们可以不用这样就可以打这个效果的断点: (lldb) bmessage -[MyViewController viewWillAppear:] 上面命令会在其父类的 viewWillAppear: 方法中打断点,并添加上了条件:[self isKindOfClass:[MyViewController class]]
3. 自定义命令
我们也可以自定义插件,不过前提是要懂一些 python。 比如设计一个打印keyWindow的windowLevel的命令:
创建python脚本文件 /magical/commands/example.py :
#!/usr/bin/python
# Example file with custom commands, located at /magical/commands/example.py
import lldb
import fblldbbase as fb
def lldbcommands():
return [ PrintKeyWindowLevel() ]
class PrintKeyWindowLevel(fb.FBCommand):
def name(self):
return 'pkeywinlevel'
def description(self):
return 'An incredibly contrived command that prints the window level of the key window.'
def run(self, arguments, options):
# It's a good habit to explicitly cast the type of all return
# values and arguments. LLDB can't always find them on its own.
lldb.debugger.HandleCommand('p (CGFloat)[(id)[(id)[UIApplication sharedApplication] keyWindow] windowLevel]')
其中定义了PrintKeyWindowLevel的类,需要实现 name description run 方法来分别告诉名称、描述、和执行实体。
创建好脚本后,然后在前面安装时创建的 ~/.lldbinit文件中添加一行:
script fblldb.loadCommandsInDirectory('/magical/commands/')
然后重启Xcode之后就可以使用自定义的命令啦。
参考链接:
http://www.jianshu.com/p/79468a2eb6db
https://blog.cnbluebox.com/blog/2015/03/05/chisel/
http://www.cocoachina.com/ios/20141219/10709.html
http://www.jianshu.com/p/3e4b10083b4d
http://www.cocoachina.com/ios/20150803/12805.html
http://www.cocoachina.com/ios/20161102/17884.html
http://blog.csdn.net/u013822374/article/details/50963108
浙公网安备 33010602011771号