一、概要
OC扩展了标准的 ANSI C编程语言,继承了C语言的优点,也延续了C语言的难点,那就是调试内存问题。尽管由于现在ARC的引入,很大程度上规避了开发上遇到的内存问题,但ARC也有其相对但局限性,比如ARC和Block特性混合使用的时候的陷阱重重,ARC环境下在使用Core Foundation类库需要注意一些问题。ARC简化了开发中的内存管理,但不能真正的替代调试,另外,掌握语言相关的内部实现高级概念及相关调试技巧,更有利于我们超越极限,不是吗?
参考:
http://zh.wikipedia.org/wiki/Objective-C
http://clang.llvm.org/docs/AutomaticReferenceCounting.html
二、主要内容
1、UncaughtExceptionHandler使用(一个开源的iOS程序异常处理类,原作者是Matt Gallagher)
2、断点的高级使用技巧
3、LLDB控制台使用
4、其他(NSZombieEnabled标志、第三方崩溃报告服务)
三、开始吧
1、UncaughtExceptionHandler是在系统提供的 NSSetUncaughtExceptionHandler异常处理类之上做了扩展和优化,是崩溃发生时候程序不会马上崩溃,更友好的提醒用户。
UncaughtExceptionHandler使用起来很简单,下载其实现文件,拷贝到项目中并且#import "UncaughtExceptionHandler.h",然后在合适到地方调用 InstallUncaughtExceptionHandler() 函数就可以了。GitHub上到例子已经表达到很清晰了,使用方法上这里不多说了。
补充一些相关概念:
a、UncaughtExceptionHandler可进行异常处理。但是对内存访问错误就无能为力了。从其初始化方法中可以看得出,UncaughtExceptionHandler处理问题大致分为两种类型:(1)NSSetUncaughtExceptionHandler类处理对异常;(2)SIGNAL类型异常
void InstallUncaughtExceptionHandler(void) { NSSetUncaughtExceptionHandler(&HandleException); signal(SIGABRT, SignalHandler); signal(SIGILL, SignalHandler); signal(SIGSEGV, SignalHandler); signal(SIGFPE, SignalHandler); signal(SIGBUS, SignalHandler); signal(SIGPIPE, SignalHandler); }
例如调用了一个没有实现的方法,这种OC级别异常NSSetUncaughtExceptionHandler可以捕捉到。
b、一些SIGNAL错误解读
SIGABRT 代表SIGNAL ABORT(中止信号),当系统发现不安全的情况时,能够对这种情况进行更多对控制,必要时候清理进程,当错误发生时候,控制台会输出大量相关错误信息,可以通过LLDB控制台命令打印其信息,后文再说这个。
SIGILL 代表SIGNAL ILLEGAL INSTRUCTION (非法指令信号),当处理器处理非法指令时候就会发生,例如传递指针时候,指针由于某种原因损坏,指向了非法内存等。
SIGSEGV 段错误信号,当系统产生一个严重当问题,例如硬件出现错误,访问不可读当内存或地址,或者向readonly当内存写入数据的时候就会报这个错。
SIGFPE 是当一个进程执行了一个错误的算术操作时发送给它的信号,一个常见的例子是由于一个意外输入导致的溢出,或者在程序构造中的错误。
SIGBUS 总线错误信号,代表无限内存访问,访问了一个无限的内存地址,地址指向的可能不是物理地址。
SIGPIPE Description Write on a pipe with no one to read it Default action Abnormal termination of the process SA_SIGINFOmacros one
c、获取函数调用栈信息
+ (NSArray *)backtrace { void* callstack[128]; int frames = backtrace(callstack, 128); char **strs = backtrace_symbols(callstack, frames); int i; NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames]; for ( i = UncaughtExceptionHandlerSkipAddressCount; i < UncaughtExceptionHandlerSkipAddressCount + UncaughtExceptionHandlerReportAddressCount; i++) { [backtrace addObject:[NSString stringWithUTF8String:strs[i]]]; } free(strs); return backtrace; }
for (NSString *mode in (NSArray *)allModes) { CFRunLoopRunInMode((CFStringRef)mode, 0.001, false); }
f、也可以将异常发到邮箱
void UncaughtExceptionHandler(NSException *exception) { NSArray *arr = [exception callStackSymbols]; NSString *reason = [exception reason]; NSString *name = [exception name]; NSString *date = [[NSDate date] description]; NSString *appInfp = GetAppInfo(); NSString *logMsg = [NSString stringWithFormat:@"======== 异常崩溃详情 ========= \ntime:%@ ============================\n%@\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", date, appInfp, name, reason, [arr componentsJoinedByString:@"\n"]]; logMsg = [logMsg stringByAppendingString:@"\n"]; NSString *urlStr = [NSString stringWithFormat:@"mailto:[email protected]?subject=%@&body=%@", @"程序异常信息", logMsg]; NSURL *url = [NSURL URLWithString:[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; [[UIApplication sharedApplication] openURL:url]; }
2、断点的高级使用技巧
先了解几个术语:GDB、LLVM 、LLDB、断点
GDB:旧版XCode的调试器
LLVM:苹果新版的调试器,替代GDB(为什么替换成LLVM的更多详情请参阅苹果文档)
LLDB:(Lower Lever Debugger)是用LLVM中可重用组件构建的下一代高性能调试器,包括完整的LLVM编译器
断点:用来暂停调试器
目前XCode5.0支持一下几种断点:(主要介绍第一种和第三种)
Exception Breakpoint:异常断点,如果项目添加此断点,当程序发生异常时候,就会断点到异常到代码(调试时候常用)
SymbolicBreakpoint:符号断点,程序执行到特定到符号时候断点起作用,其中符号可以是一个方法到名字,比如ViewDidload。
如何添加断点?
最简单办法是在工程中打开一个类,然后鼠标光标移动到要加断点到那行,然后Debug - BreakPoints ,就会发现弹出菜单中选项,可以在当前行添加断点,也可以添加一个异常断点(Exception Breakpoint)或者符号断点(Symbolic Breakpoint)等等。(快捷方式Cmd+\,禁用所有断点Cmd +Y)
注意:添加的异常断点是全局性子的,只要有异常,断点就生效;
添加符号断点会麻烦一些,如图:
其中Symbol是你想让断点在哪些符号处其作用,断点其作用时候做什么,要通过Add Action指定,可以指定断点生效时候播放一个声音或者执行一段脚本等,设置Options可以让断点不中断程序,而仅仅等相应以下提示音而已。试一试吧!此截图现实等也是工程中断点等导航页面,所有断点都在这里显示。
断点编辑:
右键断点,Edit BreakPoint,在Condition中编辑条件,然后条件满足时候就会执行断点。
进一步思考,为什么断点可以是调试器暂停,我们在编辑器中编辑代码,并且加断点,可是程序执行都是编译后的代码,是不是有种中间桥梁的方式使之完成了调试器暂停功能?答案使肯定的,项目Build后,打开Produck会发现文件中有dSYM类型文件,这个文件使调试信息文件,让源代码与编译后的代码形成映射或链接关系,这样在源码中加断点,编译后的代码一样可以执行断点(更多内容参考苹果官网)。
3、LLDB控制台使用
工程中添加断点,执行到断点之后,会弹出LLDB控制台,这个时候可以输入LLDB调试器命令来帮助测试,最常用到命令:
po 打印一个OC对象
p 打印标量变量
打印寄存器:
寄存器中保存了程序状态相关到大量信息,这些信息与处理器架构上到子函数调用规范密切相关,读懂这些信息对调试很有用,可以减少程序调试时间,当程序崩溃时候,查看寄存器中保存的那些导致程序崩溃的方法名会很利于找出问题所在。
C99语言标准中定义了register关键字,指导编译器将变量存储在CPU的寄存器中,例如:
这样就能知道出问题的大概范围。
注意:控制台打印出的寄存器信息中很多符号,都有其含义,代表执行到到方法名和地址等等。具体到含义依据系统架构而定,需灵活理解。
4、其他(NSZombieEnabled标志、第三方崩溃报告服务)
a、NSZombieEnabled:XCode可以配置此变量来调试内存相关到问题,跟踪对象释放过程。如果启用其模式,当一个对象的引用计数降到0时,系统会用一个“僵尸对象”来替换默认的对象,然后此时再对那个对象发送消息的时候,就会显示一段日志,并且跳转到调试器。
开启NSZombieEnabled模式到方法很简单:
勾选Objective-C Enable Zombie Objects就可以打开“僵尸”模式了!
b、第三方崩溃报告服务
应用提交到Appstore后,iTunes Connect会帮助记录应用到一些崩溃信息,不过苹果提供服务操作繁琐(详情见官方文档,这里不多说了),所以导致第三方服务产生了,可以提供你到App到一些使用信息及崩溃日志等功能。例如:TestFlight 或 HockeyApp,不过第三方提供等这些功能,我们完全可以通过收集崩溃日志或者其他办法去替代。