iOS开发中咱们会遇到程序异常退出的状况,若是是在调试的过程当中,可能经过设施断点或者打印关键信息的方式来进行调试,可是对于一些复杂模块非必现的异常崩溃,这种方式有时难以定位问题,并且对于已经发布上线的应用,这种方式更是无能为力。objective-c
一般咱们见到的Crash分为两种,一种是系统内存错误,触发EXC_BAD_ACCESS引发的,程序运行过程当中访问了错误的内存地址,另外一种是出现了不能被处理的signal异常,致使程序向自身发送了SIGABRT信号而崩溃,下面是我平时使用较多的crash处理方法,跟你们分享一下。架构
iOS开发中对继承自NSObject的对象,内存管理采用引用计数机制,对于非NSObject对象,引用计数机制不起做用,须要本身管理内存的使用回收。引用计数原理是,当对象被持有一次(retain),它的引用计数(retainCount)+1,被标记释放一次(release),retainCount -1,当retainCount为0,对象被释放。app
在使用手动引用计数(MRC)开发时,须要开发者显式的调用retain或release。苹果在iOS5以后推行自动引用计数(ARC),开发者没必要显式的调用retain或release了,由编译器来自动添加,在方便开发者的同时,也下降了开发难度,同时内存出现问题的时候也更难定位错误了。函数
1.2.1 添加Xcode全局异常断点学习
① 将导航器视图切换到断点导航器视图下:测试
② 点击左下角的+号,选择Exception Breakpoint这一选项。ui
③ 异常断点能够编辑许多功能,例如执行脚本,输出log,选择只处理objective-c异常等等,功能很丰富。操作系统
下一次当咱们运行程序出现崩溃的时候,程序会自动中止在崩溃的代码处,方便咱们查找问题。线程
1.2.2 僵尸对象调试debug
全局异常断点一般状况下都会把崩溃缘由定位到具体代码中。可是,若是崩溃不在当前调用栈,系统就仅仅只能把崩溃地址告诉咱们,而没办法定位到具体代码,这样咱们也无法去修改错误。相似下面这种:
这种状况下咱们能够经过Xcode提供的僵尸对象调试(Zombie Objects)来尝试找到问题。
① 首先仍是打开Xcode 选择屏幕左上角Xcode-> Preferencese,在behavior选项卡中,设置一下输出信息,调试的时候输出更多的信息,以下截图,勾上:
② 菜单Product > Scheme > Edit Scheme中,把红色圈里面的三个选项都勾上:
③ 开启该选项后,程序在运行时,若是访问了已经释放的对象,则会给出较准确的定位信息,能够帮助肯定问题所在。
该功能的原理是,在对象释放(retainCount为0)时,使用一个内置的Zombie对象,替代原来被释放的对象。不管向该对象发送什么消息(函数调用),都会触发异常,抛出调试信息。
注意:记得在问题被修复后,关闭该功能!会引发程序内存占用异常。
④ 也能够经过系统terminal打印出调用信息,使用终端的mallochistory命令,例如"mallochistory 30495 0x60005ef76fd0",其中30495是该进程的pid,pid能够根据Xcode控制台中的log查看,或者经过活动监视器得到, 根据这个记录,能够大体判断出错误代码的位置。
会出现相似如下提示代码,根据一些关键信息,就能够找出错误具体位置。
1.2.3 利用NSSetUncaughtExceptionHandler处理
以前的两种方式,对于线上的APP能够说无能为力的,还好,iOS提供了异常发生的处理API,NSSetUncaughtExceptionHandler,咱们在程序启动的时候能够添加这样的Handler,这样的程序发生异常的时候就能够对这一部分的信息进行必要的处理,适时的反馈给开发者。须要注意的是,利用NSSetUncaughtExceptionHandler能够用来处理异常崩溃,崩溃报告系统会用NSSetUncaughtExceptionHandler方法设置全局的异常处理器。若是自定义NSSetUncaughtExceptionHandler监听事件,会致使第三方监听(如Bugly)失效,已经集成了第三方监听平台的小伙伴须要注意。
① 注册全局处理异常的handler,在程序启动或者其余入口注册:
② 当线上程序出现crash时,代码会执行到以前注册的handle中,将错误信息保存在本地。
③ 经过保存的线上app的dSYM符号表查找问题。
iOS构建时产生的符号表,它是内存地址与函数名,文件名,行号的映射表。 符号表元素以下所示:
当应用crash时,咱们能够利用crash时的堆栈信息获得对应到源代码的堆栈信息,还能看到出错的代码在多少行,因此能快速定位出错的代码位置,以便快速解决问题。
获取到dSYM符号表和以前的程序崩溃的错误日志,咱们就能够定位问题了。
④ 利用atos命令定位问题。
atos命令来符号化某个特定模块加载地址 atos [-arch 架构名] [-o 符号表] [-l 模块地址] [方法地址]
使用终端计算,首先得到十六进制地址区间。
终端代码执行:
这样,就能够定位出问题代码。
Mach是Mac OS和iOS操做系统的微内核核心,Mach异常是指最底层的内核级异常,因此当APP中产生异常时,最早能监听到异常的就是Mach。
最早捕获到异常的Mach在接下来会将全部的异常转换为相应的Unix信号,并投递到出错的线程。以后就能够注册想要监听的signal类型,来捕获信号。使用Objective-C的异常处理是不能获得signal的,若是要处理它,咱们还要利用unix标准的signal机制,注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数。该函数中咱们能够输出栈信息,版本信息等其余一切咱们所想要的。以下,就是监听了SIGSEGV信号,当有SIGSEGV信号产生时,就会回调mySignalHandler方法:signal (SIGSEGV, mySignalHandler)。
信号默认的处理方法一共有五种,分别用Terminate (terminate process,即结東进程)、Ignore(忽略该信号)、Dump(terminate process and dump core:结束进程并生成 core dump,将进程的内存信息打印出来),Stop(进程暂停运行,多用于调试)以及 Cont(恢复运行个以前被暂停的进程,多用于调试)来表示。
Signal信号类型:
信号名称 |
默认处理 |
说明 |
SIGABRT |
Dump |
程序终止命令 |
SIGALRM |
Terminate |
程序超时信号 |
SIGILL |
Dump |
程序非法指令信号 |
SIGHUP |
Terminate |
程序终端停止信号 |
SIGINT |
Terminate |
程序键盘中断信号 |
SIGKILL |
Terminate |
程序强制结束信号 |
SIGTERM |
Terminate |
程序终止信号 |
SIGSTOP |
Stop |
程序键盘停止信号 |
SIGSEGV |
Dump |
程序无效内存停止信号 |
SIGBUS |
Dump |
程序内存字节未对齐停止信号 |
SIGPIPE |
Terminate |
程序Socket发送失败停止信号 |
若是没有为一个信号设置对应的处理函数,就会使用默认的处理函数,不然信号就被进程截获并调用相应的处理函数。在没有处理函数的状况下,程序能够指定两种行为:忽略这个信号 SIG_IGN 或者用默认的处理函数 SIG_DFL 。可是有两个信号是没法被截获并处理的: SIGKILL、SIGSTOP 。
① 注册全局处理异常signal的handler,在程序启动或者其余入口注册。
有关错误类型能够看上面的说明,SignalExceptionHandler是信号出错时候的回调。当有信号出错的时候,能够回调到这个方法。
② SignalHandler不要在debug环境下测试。由于系统的debug会优先去拦截。我在模拟器上运行一次后,关闭debug状态,而后直接在模拟器上点击咱们build上去的app去运行。得到以下的日志:
对于应用crash,有不少第三方优秀平台(友盟,bugly等)提供日志和打点功能,已经可以知足平常开发须要,可是学习这些经常使用的crash仍是可以帮助咱们理解iOS运行机制,以上这些是开发中常常见到的一些crash,实际处理中可能状况复杂,须要多种方式同时使用才能定位问题,灵活使用。
张力,民生科技有限公司,用户体验技术部开发工程师。