前言
iOS崩溃是让iOS开发人员比较头痛的事情,app崩溃了,说明代码写的有问题,这时如何快速定位到崩溃的地方很重要。调试阶段是比较容易找到出问题的地方的,但是已经上线的app并分析崩溃报告就比较麻烦了。
这里也会开始写一个demo,争取把所有的崩溃原因都写进demo里。
1.崩溃分析
1.1.崩溃日志(crash log)
1.1.1.xcode中查看崩溃信息
xcode->Window->Organizer->Crashes
1.1.2.根据符号表来监测奔溃位置
符号表就是指在Xcode项目编译后,在编译生成的二进制文件.app的同级目录下生成的同名的.dSYM文件。
.dSYM文件其实是一个目录,在子目录中包含了一个16进制的保存函数地址映射信息的中转文件,所有Debug的symbols都在这个文件中(包括文件名、函数名、行号等),所以也称之为调试符号信息文件。
符号表就是用来符号化 crash log(崩溃日志)。crash log中有一些方法16进制的内存地址等,通过符号表就能找到对应的能够直观看到的方法名之类。
如果是使用友盟的话,我们能在错误列表里看到一些错误,然后可以导出奔溃信息,导出的文件为.csv文件。友盟有一个分析工具,使用那个工具可以看到一些错误的函数,行号等。但是很容易分析失败,不知道为什么?
注意:使用的时候要确保你的.xcarchive在 ~/Library/Developer/Xcode/或该路径的子目录下。
.xcarchive里的.dsYM文件和.app文件是有对应的UUID的。然后你的错误详情里也是有UUID,只有当UUID相等时才能分析对。
或者自己找到.xcarchive文件和错误内存地址(友盟错误详情里标绿色的为错误内存地址)。然后通过一个小应用来分析出对应的函数。应用下载地址,具体可参考文章dSYM 文件分析工具
注意拿来分析的xcarchive名字不要有空格或特殊字符,直接用最简单的数字就好了
下图是我友盟里的错误信息,可以分析的内存地址就是标绿的地方,图中zhefengle就是你的app名,这部分后面的地址就是可以解析符号化的地址:
symbolicatecrash是xcode的一个符号化crash log的命令行工具。使用方法也就是导出.crash文件(crash log)和找到.dsYM文件,然后进行分析。
如果你有多个“.ipa”文件,多个".dSYMB"文件,你并不太确定到底“dSYM”文件对应哪个".ipa"文件,那么,这个方法就非常适合你。
特别当你的应用发布到多个渠道的时候,你需要对不同渠道的crash文件,写一个自动化的分析脚本的时候,这个方法就极其有用。
1.1.3.奔溃日志分析
以上是一个完整的崩溃日志。其实友盟错误详情里的就是上图的第4部分。
崩溃日志可以从xcode里打开Devices看到对应手机的一些奔溃信息。点击下图的View Device Logs就能看到崩溃日志。
自己写入代码,然后截取到崩溃日志,把崩溃日志发送到开发者邮箱里。
iOS Crash(崩溃)调试技巧这篇文章中有介绍如何截取崩溃日志并发送到邮箱。
Exception Type异常类型
通常包含1.7中的Signal信号和EXC_BAD_ACCESS。0xbad22222: 该编码表示 VoIP 应用因为过于频繁重启而被终止。
0xdead10cc: 读做 “dead lock”!该代码表明应用因为在后台运行时占用系统资源,如通讯录数据库不释放而被终止 。
1.2.野指针分析方法(Enable Malloc Scribble)
因为野指针的原因发生崩溃是常常出现的事,而且比较随机。关于一些原因及概念后面我们会讲到。所以我们要提高野指针的崩溃率好来帮我们快速找到有问题的代码。
对象释放后只有出现被随机填入的数据是不可访问的时候才会必现Crash。
这个地方我们可以做一下手脚,把这一随机的过程变成不随机的过程。对象释放后在内存上填上不可访问的数据,其实这种技术其实一直都有,xcode的Enable Scribble就是这个作用。
更加详细的介绍可以参考:如何定位Obj-C野指针随机Crash。
DSCrashDemo这个demo里有上面这篇文章里写的关于提高野指针崩溃率的例子。
1.3.僵尸模式(NSZombieEnabled)
所以当启用NSZombieEnabled时,一个错误的内存访问就会变成一条无法识别的消息发送给僵尸对象。僵尸对象会显示接受到得信息,然后跳入调试器,这样你就可以查看到底是哪里出了问题。
打开NSZombieEnabled之后,如果遇到对应的崩溃类型既调用了已经释放的内存空间,或者说重复释放了某个地址空间。那么就能在GDB中看到对应的输出信息。
比如会出现如下这样的问题:
[__NSArrayM addObject:]: message sent to deallocated instance 0x7179910
这样,当出现崩溃原因是message sent to deallocated instance 0x7179910,我们可以使用以下命令,把内存地址还原:
(gdb) nfo malloc-history 0x7179910
也可以使用下面的命令
(gdb) shell malloc_history {pid/partial-process-name} {address}
TODO:翻译Enabling the Malloc Debugging Features这篇文章,写对应的demo测试这类变量设置后如何找出内存出错问题。
1.4.Enable Address Sanitizer(地址消毒剂)
设置这个参数后就能看到一些更详细的错误信息提示,甚至会有内存使用情况的展示。
Address Sanitizer是另外一种解决方案。它使用了一种新的方法,有利有弊。但仍不失为一个查找代码问题的有力工具。
这类工具的理论依据是:访问内存时,通过比较访问的内存和程序实际分配的内存,验证内存访问的有效性,从而在bug发生时就检测到它们,而不会等到副作用产生时才有所察觉。
malloc函数总是最少分配16个字节。为了储存针对标准malloc的内存的保护,需要分配内存到16字节的范围内,因此,若分配的内存大小不是16字节的整数倍,余出的几个字节将不受保护。
Address Sanitizer这篇文章详细介绍了Enable Address Sanitizer,对应的中文翻译在Xcode 7上直接使用Clang Address Sanitizer
1.5.Static Analyzer(静态分析)
打开方式:Xcode->Product-Analyze
然后我们就能看到如下蓝色箭头所示的一些有问题的代码。
1.6.unrecognized selector send to instancd 快速定位
在debug navigator的断点栏里添加Create Symbolic Breakpoint。
在Symbolic中填写如下方法签名:
-[NSObject(NSObject) doesNotRecognizeSelector:]
1.7.Signal和EXC_BAD_ACCESS错误分析
1.7.1.Signal
在iOS中就是未被捕获的Objective-C异常(NSException),导致程序向自身发送了SIGABRT信号而崩溃。
就crash而言,SIGABRT是一个比较好解决的,因为他是一个可掌控的crash。App会在一个目的地终止,因为系统意识到app做了一些他不能支持的事情。
通常, SIGABRT 异常是由于某个对象接收到未实现的消息引起的。 或者,用简单的话说,在某个对象上调用了不存在的方法。
下图为源码的运行图,其中Signal中的Signal(EGV)第一次点击的时候能弹出alert,如果第二次点击就没有崩溃和alert,感觉像卡死一样。
而Signal(BRT)中的这种信号错误多次点击也是没有问题的还是能继续下去。个人猜测可能是SIGSEGV这种信号错误会导致了整个进程挂了。
注意:测试的时候如果测试Signal类型的崩溃,不要在xcode下的debug模式进行测试。因为系统的debug会优先去拦截。应该build好应用之后直接点击运行app进行测试。
1.7.2.EXC_BAD_ACCESS
2.崩溃类型收集
2.1.新老操作系统兼容
还有就是有些方法是新版操作系统才支持的,而又没有对该方法是否存在于老系统中做出判断。这种情况其实还是比较难出现的,除非开发人员太low了,因为这类方法在xcode编码时编辑器都会有提醒的。
2.2.本地存储的数据结构改变
程序在升级时,修改了本地存储的数据结构,但是对用户既存的旧数据没有做好升级,结果导致初始化时因为无法正确读取用户数据而秒退。
第一种:是把服务端传过来的一些信息保存在本地,使用的时候从本地数据库取。
然后等我升级了程序,新程序里model,定义了这个
newId
字段,但是旧版里面数据已经保存过一遍了没有这个字段。这时再去取就取不到了。所以后来我就把存储时解析数据改成了读取时解析数据。就是不管服务端传什么数据都把它存下来,然后在使用的时候再把它解析成对应的model,这样就不会丢失字段了。
第二种:自己的一些数据存储在本地SQLlite,新版的时候表结构改了。
SQLlite只支持更改一个表的名字,或者向表中增加一个字段(列),但是我们不能删除一个已经存在的字段,或者更改一个已经存在的字段的名称、数据类型、限定符等等。
这种就是有时候新版又添加字段了,或者改变了字段的名称了。一般来说原有的字段名称不应该改变,但是添加新字段是常有的事。
一般做法是在第一次创建表的时候加一些冗余字段,以防后面不时之需。但是如果真没办法需要在旧表上增加新字段了,那就要做数据迁移了。
2.3.访问的数据为空或访问数据类型不对
这类情况是比较常见的,后端传回了空数据,客户端没有做对应的判断继续执行下去了,这样就产生了crash。或者自己本地的某个数据为空数据而去使用了。还有就是访问的数据类型不是期望的数据类型而产生崩溃。
服务端都加入默认值,不返回空内容或无key,但是服务端往往会不太愿意改,还有就是有些确实应该无值的话key也不用传,减少数据量的传输。
DSCategories这里面有两个Category
NSArray+SafeAccessM
和NSDictionary+SafeAccess
可以看一下。
2.4.操作了不该操作的对象,野指针之类
2.4.1野指针介绍
2.4.2野指针崩溃情况
野指针访问已经释放的对象crash其实不是必现的,因为dealloc执行后只是告诉系统,这片内存我不用了,而系统并没有就让这片内存不能访问。
- 对象释放后内存没被改动过,原来的内存保存完好,可能不Crash或者出现逻辑错误(随机Crash)。
- 对象释放后内存没被改动过,但是它自己析构的时候已经删掉某些必要的东西,可能不Crash、Crash在访问依赖的对象比如类成员上、出现逻辑错误(随机Crash)。
- 对象释放后内存被改动过,写上了不可访问的数据,直接就出错了很可能Crash在objc_msgSend上面(必现Crash,常见)。
- 对象释放后内存被改动过,写上了可以访问的数据,可能不Crash、出现逻辑错误、间接访问到不可访问的数据(随机Crash)。
- 对象释放后内存被改动过,写上了可以访问的数据,但是再次访问的时候执行的代码把别的数据写坏了,遇到这种Crash只能哭了(随机Crash,难度大,概率低)!!
- 对象释放后再次release(几乎是必现Crash,但也有例外,很常见)。
2.5.内存处理不当
说到因为内存处理不当崩溃就要涉及到内存管理问题了。内存管理是软件开发中一个重要的课题。iOS自从引入ARC机制后,对于内存的管理开发者好像轻松了很多,但是还会发生一些内存泄露之类的问题。
对于这一块知识点需要了解ARC的一些机制,还有用instruments排查内存泄露问题等。这部分我后面会专门写一篇文章进行阐述。
2.6.主线程UI长时间卡死,被系统杀掉
主线程被卡住是非常常见的场景,具体表现就是程序不响应任何的UI交互。这时按下调试的暂停按钮,查看堆栈,就可以看到是到底是死锁、死循环等,导致UI线程被卡住。
2.7.多线程之间切换访问引起的crash
多线程引起的崩溃大部分是因为使用数据库的时候多线程同时读写数据库而造成了crash。
多线程导致的iOS闪退分析这篇文章就是关于多线程crash的调试。