iOS崩溃是让iOS开发人员比较头痛的事情,app崩溃了,说明代码写的有问题,这时如何快速定位到崩溃的地方很重要。调试阶段是比较容易找到出问题的地方的,可是已经上线的app并分析崩溃报告就比较麻烦了。php
以前我老是找到一个改一个,并靠别人测试重现来找出问题的地方,这样每每比较耗费时间。并且比较难找到缘由的时候每次都是到网上找各类资源搜索,解决了以后也没有认真分析缘由及收集,时间长了以后就会忘记原来解决过的问题,别人来问个人时候我也不能很快找到答案。因此这里写一个关于崩溃的文章,以便以后本身查询用。html
这里也会开始写一个demo,争取把全部的崩溃缘由都写进demo里。ios
xcode->Window->Organizer->Crashesgit
什么是符号表github
符号表就是指在Xcode项目编译后,在编译生成的二进制文件.app的同级目录下生成的同名的.dSYM文件。shell
.dSYM文件实际上是一个目录,在子目录中包含了一个16进制的保存函数地址映射信息的中转文件,全部Debug的symbols都在这个文件中(包括文件名、函数名、行号等),因此也称之为调试符号信息文件。数据库
符号表有什么用后端
符号表就是用来符号化 crash log(崩溃日志)。crash log中有一些方法16进制的内存地址等,经过符号表就能找到对应的可以直观看到的方法名之类。xcode
如何获得.dsYM文件安全
咱们在Archive的时候会生成.xcarchive文件,而后显示包内容就可以在里面找到.dsYM文件和.app文件。
如何使用.dsYM
1.友盟.dsYM分析
若是是使用友盟的话,咱们能在错误列表里看到一些错误,而后能够导出奔溃信息,导出的文件为.csv文件。友盟有一个分析工具,使用那个工具能够看到一些错误的函数,行号等。可是很容易分析失败,不知道为何?
注意:使用的时候要确保你的.xcarchive在 ~/Library/Developer/Xcode/或该路径的子目录下。
.xcarchive里的.dsYM文件和.app文件是有对应的UUID的。而后你的错误详情里也是有UUID,只有当UUID相等时才能分析对。
我犯的错误:由于咱们是两我的开发,Archive的时候都是在另外一我的的电脑上Archive的,因此个人电脑里根本没有对应的.xcarchive文件。因此我在我电脑上用友盟的分析工具分析是时候是监测不出来错误的。
2.第三方小工具.dsYM分析
或者本身找到.xcarchive文件和错误内存地址(友盟错误详情里标绿色的为错误内存地址)。而后经过一个小应用来分析出对应的函数。应用下载地址,具体可参考文章dSYM 文件分析工具
下图是我友盟里的错误信息,能够分析的内存地址就是标绿的地方,图中zhefengle就是你的app名,这部分后面的地址就是能够解析符号化的地址:
下图是分析工具分析上面的错误内存地址:
分析工具
3.命令行工具symbolicatecrash
symbolicatecrash是xcode的一个符号化crash log的命令行工具。使用方法也就是导出.crash文件(crash log)和找到.dsYM文件,而后进行分析。
4.还有命令行工具atos
若是你有多个“.ipa”文件,多个".dSYMB"文件,你并不太肯定到底“dSYM”文件对应哪一个".ipa"文件,那么,这个方法就很是适合你。
特别当你的应用发布到多个渠道的时候,你须要对不一样渠道的crash文件,写一个自动化的分析脚本的时候,这个方法就极其有用。
简单使用方法命令行工具atos
参考iOS应用崩溃日志分析里面有很详细的分析介绍。
崩溃日志
以上是一个完整的崩溃日志。其实友盟错误详情里的就是上图的第4部分。
如何获得崩溃日志
1.把设备连上电脑,获得本身设备的崩溃日志
崩溃日志能够从xcode里打开Devices看到对应手机的一些奔溃信息。点击下图的View Device Logs就能看到崩溃日志。
2.使用第三方崩溃管理工具
我暂时只使用过友盟,友盟里面有错误分析,就是截取的崩溃日志。
3.本身截取崩溃日志
本身写入代码,而后截取到崩溃日志,把崩溃日志发送到开发者邮箱里。
iOS Crash(崩溃)调试技巧这篇文章中有介绍如何截取崩溃日志并发送到邮箱。
分析崩溃日志
崩溃日志中的(3)异常
Exception Type异常类型
一般包含1.7中的Signal信号和EXC_BAD_ACCESS。Exception Codes:异常编码
0x8badf00d: 读作 “ate bad food”! (把数字换成字母,是否是很像 :p)该编码表示应用是由于发生watchdog超时而被iOS终止的。 一般是应用花费太多时间而没法启动、终止或响应用系统事件。0xbad22222: 该编码表示 VoIP 应用由于过于频繁重启而被终止。
0xdead10cc: 读作 “dead lock”!该代码代表应用由于在后台运行时占用系统资源,如通信录数据库不释放而被终止 。
0xdeadfa11: 读作 “dead fall”! 该代码表示应用是被用户强制退出的。根据苹果文档, 强制退出发生在用户长按开关按钮直到出现 “滑动来关机”, 而后长按 Home按钮。强制退出将产生 包含0xdeadfa11 异常编码的崩溃日志, 由于大多数是强制退出是由于应用阻塞了界面。
崩溃日志中的(4)线程回溯
这部分提供应用中全部线程的回溯日志。 回溯是闪退发生时全部活动帧清单。它包含闪退发生时调用函数的清单。看下面这行日志:
它包括四列:
帧编号—— 此处是2。(数子从大到小为发生的顺序)
二进制库的名称 ——此处是 XYZLib.
调用方法的地址 ——此处是 0x34648e88.
第四列分为两个子列,一个基本地址和一个偏移量。此处是0×83000 + 8740, 第一个数字指向文件,第二个数字指向文件中的代码行。
介绍
由于野指针的缘由发生崩溃是经常出现的事,并且比较随机。关于一些缘由及概念后面咱们会讲到。因此咱们要提升野指针的崩溃率好来帮咱们快速找到有问题的代码。
对象释放后只有出现被随机填入的数据是不可访问的时候才会必现Crash。
这个地方咱们能够作一下手脚,把这一随机的过程变成不随机的过程。对象释放后在内存上填上不可访问的数据,其实这种技术其实一直都有,xcode的Enable Scribble就是这个做用。
enter image description here
更加详细的介绍能够参考:如何定位Obj-C野指针随机Crash。
DSCrashDemo这个demo里有上面这篇文章里写的关于提升野指针崩溃率的例子。
介绍
启用了NSZombieEnabled的话,它会用一个僵尸来替换默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现会将该对象转换成僵尸对象。僵尸对象的做用是在你向它发送消息时,它会显示一段日志并自动跳入调试器。
因此当启用NSZombieEnabled时,一个错误的内存访问就会变成一条没法识别的消息发送给僵尸对象。僵尸对象会显示接受到得信息,而后跳入调试器,这样你就能够查看究竟是哪里出了问题。
因此这时通常崩溃的缘由是:调用了已经释放的内存空间,或者说重复释放了某个地址空间。
如何找出问题
1.NSZombieEnabled
打开NSZombieEnabled以后,若是遇到对应的崩溃类型既调用了已经释放的内存空间,或者说重复释放了某个地址空间。那么就能在GDB中看到对应的输出信息。
好比会出现以下这样的问题:
[__NSArrayM addObject:]: message sent to deallocated instance 0x7179910
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测试这类变量设置后如何找出内存出错问题。
设置这个参数后就能看到一些更详细的错误信息提示,甚至会有内存使用状况的展现。
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
Static Analyzer是一个很是好的工具去发现编译器警告不会提示的问题和一些我的的内错泄露和死存储(不会用到的赋了值的变量)错误。这个方法可能大大的提升内存使用和性能,以及提高应用的总体稳定性和代码质量。
打开方式:Xcode->Product-Analyze
而后咱们就能看到以下蓝色箭头所示的一些有问题的代码。
在debug navigator的断点栏里添加Create Symbolic Breakpoint。
在Symbolic中填写以下方法签名:
-[NSObject(NSObject) doesNotRecognizeSelector:]
设置完成后再遇到相似的错误就会定位到具体的代码。
什么是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异常捕获这篇文章中有对各类信号的解释。
SIGABRT
就crash而言,SIGABRT是一个比较好解决的,由于他是一个可掌控的crash。App会在一个目的地终止,由于系统意识到app作了一些他不能支持的事情。
一般, SIGABRT 异常是因为某个对象接收到未实现的消息引发的。 或者,用简单的话说,在某个对象上调用了不存在的方法。
SIGSEGV
SIGSEGV程序无效内存停止信号,通常是表示内存不合法,
SIGBUS
SIGBUS程序内存字节未对齐停止信号,
截取Signal和Exception从容的崩溃
DSSignalHandlerDemo
这是一个防止奔溃的源码,可使一些本来会奔溃的操做弹出UIAlertView。里面写了两种信号量的崩溃:SIGSEGV、SIGABRT,还有一些信号你们能够写上去提个PR给我。下图为源码的运行图,其中Signal中的Signal(EGV)第一次点击的时候能弹出alert,若是第二次点击就没有崩溃和alert,感受像卡死同样。
而Signal(BRT)中的这种信号错误屡次点击也是没有问题的仍是能继续下去。我的猜想多是SIGSEGV这种信号错误会致使了整个进程挂了。
注意:测试的时候若是测试Signal类型的崩溃,不要在xcode下的debug模式进行测试。由于系统的debug会优先去拦截。应该build好应用以后直接点击运行app进行测试。
EXC_BAD_ACCESS是一个比较难处理的crash了,当一个app进入一种毁坏的状态,一般是因为内存管理问题而引发的时,就会出现出现这样的crash。一般1.7.1中的Signal信号错误都会提醒EXC_BAD_ACCESS。
文中1.3就介绍了用一些变量设置来找出这类错误。
缘由
开发人员在进行开发的时候,经常使用的是某个操做系统版本,因此在开发人员进行开发测试的那个系统版本上基本不会出现问题。但在其余版本上开发人员没法进行彻底的测试,这就致使了在新系统上运行正常,但在旧系统上却崩溃的状况。
在新 iOS 上正常的应用,到了老版本 iOS 上秒退最多见缘由是系统动态连接库或Framework没法找到。这种状况一般是因为 App 引用了一个新版操做系统里的动态库(或者某动态库的新版本)或只有新 iOS 支持的 Framework,而又没有对老系统进行测试,因而当 App 运行在老系统上时便因为找不到而秒退。
还有就是有些方法是新版操做系统才支持的,而又没有对该方法是否存在于老系统中作出判断。这种状况其实仍是比较难出现的,除非开发人员太low了,由于这类方法在xcode编码时编辑器都会有提醒的。
解决
这种问题通常就是用户升级操做系统或者开发人员修改问题以兼容老系统。
缘由
程序在升级时,修改了本地存储的数据结构,可是对用户既存的旧数据没有作好升级,结果致使初始化时由于没法正确读取用户数据而秒退。
解决
第一种:是把服务端传过来的一些信息保存在本地,使用的时候从本地数据库取。
刚开始的时候我是第一次从服务端获得数据的时候直接解析成对应的model而后存入plist文件里面。这时就有一个问题,好比服务端新传了字段
newId
,可是我旧版model里面没有定义过,存入本地的数据仍是没有这个字段。而后等我升级了程序,新程序里model,定义了这个
newId
字段,可是旧版里面数据已经保存过一遍了没有这个字段。这时再去取就取不到了。因此后来我就把存储时解析数据改为了读取时解析数据。就是无论服务端传什么数据都把它存下来,而后在使用的时候再把它解析成对应的model,这样就不会丢失字段了。
第二种:本身的一些数据存储在本地SQLlite,新版的时候表结构改了。
SQLlite只支持更改一个表的名字,或者向表中增长一个字段(列),可是咱们不能删除一个已经存在的字段,或者更改一个已经存在的字段的名称、数据类型、限定符等等。
这种就是有时候新版又添加字段了,或者改变了字段的名称了。通常来讲原有的字段名称不该该改变,可是添加新字段是常有的事。
通常作法是在第一次建立表的时候加一些冗余字段,以防后面不时之需。可是若是真没办法须要在旧表上增长新字段了,那就要作数据迁移了。
这里有一个库在FMDB基础上管理SQLlite数据库了,能够用来作数据迁移用。FMDBMigrationManager
TODO:作一个数据迁移的demo
缘由
这类状况是比较常见的,后端传回了空数据,客户端没有作对应的判断继续执行下去了,这样就产生了crash。或者本身本地的某个数据为空数据而去使用了。还有就是访问的数据类型不是指望的数据类型而产生崩溃。
解决
第一种:
服务端都加入默认值,不返回空内容或无key,可是服务端每每会不太愿意改,还有就是有些确实应该无值的话key也不用传,减小数据量的传输。
第二种:
这种就是客户端本身作判断,若是每次都是本身去if判断是否为空或格式是否正确那确定是比较麻烦的。因此这里用到了NSArray和NSDictionary的Category。通常咱们访问的数据都是NSArray或NSDictionary,因此在取值方法里面作一下判断,返回正确的数据类型或默认值便可。
DSCategories这里面有两个Category
NSArray+SafeAccessM
和NSDictionary+SafeAccess
能够看一下。
iOS中有空指针和野指针两种概念。
空指针是没有存储任何内存地址的指针。如
Student s1 = NULL;
和Student s2 = nil;
而野指针是指指向一个已删除的对象("垃圾"内存既不可用内存)或未申请访问受限内存区域的指针。野指针是比较危险的。由于野指针指向的对象已经被释放了,不能用了,你再给被释放的对象发送消息就是违法的,因此会崩溃。
空指针和野指针这篇文章介绍了空指针和野指针的概念。
野指针访问已经释放的对象crash其实不是必现的,由于dealloc执行后只是告诉系统,这片内存我不用了,而系统并无就让这片内存不能访问。
因此野指针的奔溃是比较随机的,你在测试的时候可能没发生crash,可是用户在使用的时候就可能发生crash了。
注意:arc环境比非arc环境更少出现野指针。
现实出现问题大概是下面几种可能的状况:
- 对象释放后内存没被改动过,原来的内存保存无缺,可能不Crash或者出现逻辑错误(随机Crash)。
- 对象释放后内存没被改动过,可是它本身析构的时候已经删掉某些必要的东西,可能不Crash、Crash在访问依赖的对象好比类成员上、出现逻辑错误(随机Crash)。
- 对象释放后内存被改动过,写上了不可访问的数据,直接就出错了极可能Crash在objc_msgSend上面(必现Crash,常见)。
- 对象释放后内存被改动过,写上了能够访问的数据,可能不Crash、出现逻辑错误、间接访问到不可访问的数据(随机Crash)。
- 对象释放后内存被改动过,写上了能够访问的数据,可是再次访问的时候执行的代码把别的数据写坏了,遇到这种Crash只能哭了(随机Crash,难度大,几率低)!!
- 对象释放后再次release(几乎是必现Crash,但也有例外,很常见)。
参考下面这张图:
enter image description here
说到由于内存处理不当崩溃就要涉及到内存管理问题了。内存管理是软件开发中一个重要的课题。iOS自从引入ARC机制后,对于内存的管理开发者好像轻松了不少,可是还会发生一些内存泄露之类的问题。
对于这一块知识点须要了解ARC的一些机制,还有用instruments排查内存泄露问题等。这部分我后面会专门写一篇文章进行阐述。
主线程被卡住是很是常见的场景,具体表现就是程序不响应任何的UI交互。这时按下调试的暂停按钮,查看堆栈,就能够看到是究竟是死锁、死循环等,致使UI线程被卡住。
这部分须要研究多线程,还有如何看调试栏里的线程的信息。
GCD死锁这篇文章介绍了GCD使用多线程时的死锁问题。
还有这篇iOS多线程中,队列和执行的排列组合结果分析也值得参考
TODO:写死锁demo
多线程引发的崩溃大部分是由于使用数据库的时候多线程同时读写数据库而形成了crash。
多线程致使的iOS闪退分析这篇文章就是关于多线程crash的调试。