前言
Bebug调试程序是开发中最常见的问题,对于一些简单有效的调试技巧的了解是很有必要的。这篇文章就列举Debug中用到的一些简单的技巧。
一.打印
相信在调试程序时,打印有时候一定是少不了的,当然你也可以用LLDB命令完全代替,但是打印技巧依然是比较实用的一种调试技巧。
#ifdef DEBUG
#define NSLog(...) NSLog(@"%s %@", __PRETTY_FUNCTION__, [NSString stringWithFormat:__VA_ARGS__])
#else
#define NSLog(...) do { } while (0)
#endif
#endif
这是一个使用了条件编译和宏重写的 NSLog,在打印出信息的时候,会打印出当前类名称和方法名称。
二.断点
(1)普通断点
普通断点是调试中最常使用的。当程序运行到断点处就会暂停运行。
用于在某些重要的操作前查看关键参数的值。
(2)条件断点
点击Xcode editor的‘gutter’来添加断点,右键点击断点,然后选择“edit breakpoint”来设置特定条件。
你还可以添加能根据断点自动发生的动作,例如一个debugger command---打印一个值,
以下是亲测可用的条件断点语句:
dic.count == 3 [dic count] == 4
cmd == "login" 不是 cmd ==@"login" [cmd isEqualToString:@"login"]
i==1
[[dic objectForKey:@"errorCode"]isEqualToString:@"10"]
!dic
在Edit Breakpoint...窗口中有下面四个输入项:
Condition 设置的条件。
Ignore 表示忽略多少次之后断点有效。例如:不设置条件,Ignore设置为4,则i=4时程序在断点处停止。
Action 在程序断点处执行的操作(执行完这个操作后代码停止运行,此时设置断点处的代码还没有执行)。这里的操作是LLDB语句,关于LLDB会在下面介绍。
Options 当选中时,执行完Action的操作后代码不会停止,就像没有设置断点一样。
(3)异常断点Exception BreakPoint
在设置异常断点的情况下 当程序crash时 Xcode会帮我们定位到crash产生的位置。
设置异常断点步骤:
(4)符号断点Symbolic Breakpoint
符号断点能够为某一个方法或者 某一个类的某一个方法设置断点。实现的功能如下图:
某一个类的某一个方法设置断点
在某个方法中执行断点
设置步骤如下:
如果你的Symbol只写了一个函数名,那么就会在出现该函数名的地方就中断执行。如下,就会在运行到doAnimation的时候中断。是不是很强大呢?
三.LLDB命令
LLDB的Xcode默认的调试器, 我们通过执行LLDB命令使调试过程更加的灵活。并且可以通过指令立刻看到一些我们需要修改完代码再次运行才能看到的效果或者结果,超级实用。Xcode内嵌LLDB调试窗口。在程序执行到断点后你可以输入LLDB命令操作调试过程。
LLDB常用命令如下:
1, p (print)用于输出基本类型, 如 p (int)[[[self view] subviews] count] 输出子视图个数。(而且还能输出字符串的地址,很实用,想看字符串指针指向的地址就不用 NSLog啦。)
2, po (print object)输出对象, 如 po [self view]; (相当于普通的NSLog)
3, expr (expression) 可以在调试时动态执行指定表达式,并将结果打印出来。常用于在调试过程中修改变量的值。例如上图所示,程序第一次执行到断点时
执行下面的指令:expr i=4
你会看到如下的输出: (int) $0 = 4
继续运行程序,程序输出的信息是:value:4 i==4 (这个功能相当于,不修改代码再次运行的情况下,动态修改参数值看程序的执行结果,很强大。)
4, call call即是调用的意思。其实上述的po和p也有调用的功能。因此一般只在不需要显示输出,或是方法无返回值时使用call。我们可以在viewDidLoad:里面设置断点,然后在程序中断的时候输入下面的命令: call [self.view setBackgroundColor:[UIColor redColor]] 此时view的背景颜色变为红色。(这个相当于,动态往程序中加入新的代码,不用修改代码再次运行即可看到一些你想看到的效果,超级强大。)
5, bt 打印当前线程的调用堆栈,加all可打印所有thread的堆栈。不详细举例说明,感兴趣的朋友可以自己试试。
6, fr v -R 命令来打印出变量的未加工过时的信息,
7.help 最简单命令是 help,它会列举出所有的命令。如果你忘记了一个命令是做什么的,或者想知道更多的话,你可以通过 help来了解更多细节,例如 help print或者 help thread如果你甚至忘记了help命令是做什么的,你可以试试help help不过你如果知道这么做,那就说明你大概还没忘光这个命令。
如果想了解更详细的内容,戳这里。
控制台左侧的调试区:
在左侧调试区 右键 选择“Add Expression” 输入你想要显示的变量名称,即可立即显示(注意这个变量不可以是不直观的)。这种方法一般用于你在断点 debug时,鼠标光标放上去的时候不显式某个你想要的值,可以这样让其显示
这种方法更加的强大,在断点debug的时候,完全代替了“想看某一个隐形值,左边调试区又看不到,自己加一个 NSlog ,关闭程序,再次运行”的尴尬,再次运行一个庞大项目是很耗时间的,就为了加一个 NSLOG???,
po 命令:为 print object 的缩写,显示对象的文本描述(显示从对象的 description 消息获得的字符串信息)。
(它甚至可以打印一些通过方法才能得到的值,如下,很强大,有人说左边不是也可以看吗,左边是可以看,但是需要一层一层打开,不够直观)
四. Scheme中run状态下的 Diagnostics(诊断)
野指针分析方法(Enable Malloc Scribble)
因为野指针的原因发生崩溃是常常出现的事,而且比较随机。关于一些原因及概念后面我们会讲到。所以我们要提高野指针的崩溃率好来帮我们快速找到有问题的代码。对象释放后只有出现被随机填入的数据是不可访问的时候才会必现Crash。
这个地方我们可以做一下手脚,把这一随机的过程变成不随机的过程。对象释放后在内存上填上不可访问的数据,其实这种技术其实一直都有,xcode的Enable Scribble就是这个作用。
enter image description here
更加详细的介绍可以参考:如何定位Obj-C野指针随机Crash。
DSCrashDemo这个demo里有上面这篇文章里写的关于提高野指针崩溃率的例子。
僵尸模式(NSZombieEnabled)
启用了NSZombieEnabled的话,它会用一个僵尸来替换默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现会将该对象转换成僵尸对象。僵尸对象的作用是在你向它发送消息时,它会显示一段日志并自动跳入调试器。
所以当启用NSZombieEnabled时,一个错误的内存访问就会变成一条无法识别的消息发送给僵尸对象。僵尸对象会显示接受到得信息,然后跳入调试器,这样你就可以查看到底是哪里出了问题。
所以这时一般崩溃的原因是:调用了已经释放的内存空间,或者说重复释放了某个地址空间。
如何找出问题
1.NSZombieEnabled
EXC_BAD_ACCESS 可以这么说,90%的错误来源在于对一个已经释放的对象进行release操作。
最后提醒NSZombieEnabled只能在调试的时候使用,千万不要忘记在产品发布的时候去掉,因为NSZombieEnabled不会真正去释放dealloc对象的内存,一直开启后果可想而知。
就是当设置NSZombieEnabled环境变量后,一个对象销毁时会被转化为_NSZombie,设置NSZombieEnabled后,当你向一个已经释放的对象发送消息,这个对象就不会向之前那样Crash或者产生一个难以理解的行为,而是放出一个错误消息,然后以一种可预测的可以产生debug断点的方式消失, 因此我们就可以找到具体或者大概是哪个对象被错误的释放了。
对 Xcode 设置了NSZombieEnabled 之后,Xcode 会明确定位在行[array addObject:@"Hello"],然后控制台下报的错误信息是:
*** -[__NSArray addObject:]:message sent to deallocated instance 0x6557370
2.MallocStackLoggingNoCompact
如果崩溃是发生在当前调用栈,通过上面的做法,系统就会把崩溃原因定位到具体代码中。但是,如果崩溃不在当前调用栈,系统就仅仅只能把崩溃地址告诉我们,而没办法定位到具体代码,这样我们也没法去修改错误。这时就可以修改scheme,让xcode记录每个地址alloc的历史,这样我们就可以用命令把这个地址还原出来。
如图:(跟设置NSZombieEnabled一样,添加MallocStackLoggingNoCompact,并且设置为YES)
这样,当出现崩溃原因是message sent to deallocated instance 0x7179910,我们可以使用以下命令,把内存地址还原:
(gdb) nfo malloc-history 0x7179910
也可以使用下面的命令
(gdb) shell malloc_history {pid/partial-process-name} {address}
这篇文章中有介绍MallocStackLoggingNoCompact的使用。
还有官方文档Enabling the Malloc Debugging Features介绍了类似NSZombieEnabled和MallocStackLoggingNoCompact这类的环境变量的作用。
TODO:翻译Enabling the Malloc Debugging Features这篇文章,写对应的demo测试这类变量设置后如何找出内存出错问题。
Enable Address Sanitizer(地址消毒剂)
设置这个参数后就能看到一些更详细的错误信息提示,甚至会有内存使用情况的展示。
C语言是一门危险的语言,内存安全是一个主要的问题。C语言中根本没有内存安全可言。像下面的代码,会被正常的编译,而且可能正常运行:
char *ptr = malloc(5);
ptr[12] = 0;
对于内存安全的验证已经有一些解决方案了。如Clang的静态代码分析,可以从代码中查找特定类型的内存安全问题。如Valgrind之类的程序可以在运行时检测到不安全的内存访问。
Address Sanitizer是另外一种解决方案。它使用了一种新的方法,有利有弊。但仍不失为一个查找代码问题的有力工具。
这类工具的理论依据是:访问内存时,通过比较访问的内存和程序实际分配的内存,验证内存访问的有效性,从而在bug发生时就检测到它们,而不会等到副作用产生时才有所察觉。
malloc函数总是最少分配16个字节。为了储存针对标准malloc的内存的保护,需要分配内存到16字节的范围内,因此,若分配的内存大小不是16字节的整数倍,余出的几个字节将不受保护。
Address Sanitizer会追踪受限内存,使用了一种简单但是很巧妙的方法:它在进程的内存空间上保存了一个固定的区域,叫做“影子内存区”。用内存消毒剂的术语来说,一个被标记为受限的内存被称作“中毒”内存。“影子内存区”会记录哪些内存字节是中毒的。通过一个简单的公式,可以将进程中的内存空间映射到“影子内存区”中,即:每8字节的正常内存块映射到一个字节的影子内存上。在影子内存上,会跟踪这8字节的“中毒状态”。
Address Sanitizer这篇文章详细介绍了Enable Address Sanitizer,对应的中文翻译在Xcode 7上直接使用Clang Address Sanitizer
Signal和EXC_BAD_ACCESS错误分析
什么是Signal
在计算机科学中,信号(英语:Signals)是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。
在iOS中就是未被捕获的Objective-C异常(NSException),导致程序向自身发送了SIGABRT信号而崩溃。
Signal信号的类型
SIGABRT–程序中止命令中止信号
SIGALRM–程序超时信号
SIGFPE–程序浮点异常信号
SIGILL–程序非法指令信号
SIGHUP–程序终端中止信号
SIGINT–程序键盘中断信号
SIGKILL–程序结束接收中止信号
SIGTERM–程序kill中止信号
SIGSTOP–程序键盘中止信号
SIGSEGV–程序无效内存中止信号
SIGBUS–程序内存字节未对齐中止信号
SIGPIPE–程序Socket发送失败中止信号
iOS异常捕获这篇文章中有对各种信号的解释。
EXC_BAD_ACCESS
EXC_BAD_ACCESS是一个比较难处理的crash了,当一个app进入一种毁坏的状态,通常是由于内存管理问题而引起的时,就会出现出现这样的crash。通常1.7.1中的Signal信号错误都会提醒EXC_BAD_ACCESS。
五.Static Analyzer(静态分析)
Static Analyzer是一个非常好的工具去发现编译器警告不会提示的问题和一些个人的内错泄露和死存储(不会用到的赋了值的变量)错误。这个方法可能大大的提高内存使用和性能,以及提升应用的整体稳定性和代码质量。
打开方式:Xcode->Product-Analyze
然后我们就能看到如下蓝色箭头所示的一些有问题的代码。
使用Xcode来分析你的项目,从Xcode的 Product菜单选择 Analyze或按 Shift-Command-B.Xcode的将需要片刻的时间,但是当它完成的时候你会在左边的 Issue Navigator看到问题列表。由Analyze发现的问题用蓝色高亮显示。
当你点击一个问题,Xcode的会指向问题代码块,这些正是你要的注意的地方。注意,Xcode仅仅是建议。在某些情况下,这是可能的,问题是不相关的,不固定。如果你找不到造成EXC_BAD_ACCESS的错误,那就需要你仔细审视Xcode项目,分析其中发现的每一个问题。
六.Instruments检查器
提起检查器,我们有时会忽略Xcode本身自带的显示CPU,内存,网络的测试界面的使用,尤其是对流量的统计是比较实用的。
Instruments是一个强大而灵活的性能分析和测试工具,它是Xcode工具集的一部分。它旨在帮助您分析您的OS X和iOS应用程序,过程和设备,以便更好地了解和优化其行为和性能。从开发应用程序开发流程到将工具集成到您的工作流程中,可以帮助您在开发周期的早期找到问题,从而节省您的时间。
七. 调试工具集:FLEX
FLEX是Flipboard开源的一系列在应用中调试的工具集。FLEX以第三方库的形式集成在应用中,使用时将类库加到工程中,然后 通过调用[[FLEXManager sharedManager] showExplorer];就可显示出用于调试的工具栏进行调试。
它提供的功能如下:
查看、修改views
查看任何对象的属性
动态的修改属性
动态的调用实例方法和类方法
查看网络请求过程
添加模拟的键盘快捷键
查看系统日志
从堆中获取任何对象
查看沙盒中的文件
查看文件系统中的SQLite/Realm数据库
在模拟器中触发3D touch
查看你应用中所有的类
快速获取常用的类,例如[UIApplication sharedApplication], the app delegate, the root view controller on the key window, and more.
动态的查看NSUserDefaults里面的值
简直吊炸天。当你将FLEX集成到你项目中时就会认识到它的威力。看起来确实很强大,可是我并没有使用过,不做评价,以后使用后再更新相关信息。
小结
程序的调试包括很多方面,我这篇文章可能并不全面,但是也算是一个小结,后续会持续更新。
本文参考文章: