关于iOS崩溃的认知和一些避免
一个APP的崩溃率是对一个前端人员是否为合格的一个审核标准.
那么在每天日常中开发迭代中的那么多崩溃率信息有哪些呢?
又是因为什么原因导致的呢?该怎么去避免?
APP常见崩溃:
- Container crash(数组越界,插nil等)
- 字典的构造与修改
- 操作 UITableView UICollectionView 数据增删改读操作
- NSString crash (字符串操作的crash)
- unrecognized selector
- UI not on Main Thread Crash (非主线程刷UI (机制待改善))
1.数组下标的越界
简易示例代码:
- (void)testArrayOut {
NSArray *array = @[@"a", @"b", @"c"];
NSLog(@"%@", array[4]);
}
崩溃信息
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 4 beyond bounds [0 .. 2]'
一般对数组的操作出现Crash的情况:
* 取值:index超出array的索引范围
* 添加:新增的object为nil或者Null
* 插入:index大于count、插入的object为nil或者Null
* 删除:index超出array的索引范围
* 替换:index超出array的索引范围、替换的object为nil或者Null
2.字典的构造与修改
简易示例代码:
- (void)testDictionCrash {
NSString *aKey = nil;
NSDictionary *dictionary = @{@"a": aKey};
NSLog(@"%@", dictionary);
}
一般对字典操作出现Crash的情况:
- NSDictionary 不支持 nil 作为 key.
- NSDictionary 不支持 nil 作为 value.
崩溃信息
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0]'
以上预防方法:
- 替换系统方法.通过 Method Swizzling 替换成自己的方法,然后再执行方法的时候加以判断.
- 使用类别扩展一个方法自定义实现对应操作后的预防.
- 万能基佬王:http://t.cn/R0CTokN
- 万能谷歌法:http://t.cn/R0CTNZE
3.操作 UITableView UICollectionView 数据增删改读操作
简易示例代码:
- (void)testTableViewUpdateCrash {
NSIndexPath *insertIndexPath = [NSIndexPath indexPathForRow:10 inSection:0];
NSIndexPath *deleteIndexPath = [NSIndexPath indexPathForRow:11 inSection:0];
NSIndexPath *reloadIndexPath = [NSIndexPath indexPathForRow:12 inSection:0];
NSIndexPath *moved1IndexPath = [NSIndexPath indexPathForRow:13 inSection:0];
NSIndexPath *moved2IndexPath = [NSIndexPath indexPathForRow:14 inSection:0];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:@[insertIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView deleteRowsAtIndexPaths:@[deleteIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView reloadRowsAtIndexPaths:@[reloadIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView moveRowAtIndexPath:moved1IndexPath toIndexPath:moved2IndexPath];
[self.tableView endUpdates];
}
崩溃信息
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete row 12 from section 0 which only contains 10 rows before the update'
一般出现Crash情况:
- 1.返回的数据源 和 insert / delete 后,section所包含的行数不一致.
- 2.刷新对应行数的UI没有对应上数据源.
预防方法:
- 1.当需要动态更新tableView的数据时,计算好模型的数据使模型的数据和更新tableView的后的数据保持同步。
- 2.在调用deleteRowsAtIndexPaths:方法前,要确保数据为最新。也就是说,先将要删除的数据从数据源中删除。
- 3.分组和分组中行数是变动的,不能写死.
- 4.先操作数据源,再操作UITableView对应的刷新动画
4.NSAttributedString,NSMutableString 等可变对象操作相关
简易示例代码
- (void)testMutableAttributedStringCrash {
NSString *string = nil;
NSMutableString *mutableString = [[NSMutableString alloc] initWithString:string];
NSLog(@"%@",mutableString);
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string];
NSLog(@"%@",attributedString);
}
崩溃信息
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'NSConcreteMutableAttributedString initWithString:: nil value'
一般出现Crash情况:
1.初始化一个空的String.
2.拼接,插入,替换等操作一个nil的值.
预防方法:
- 检查参数和参数类型的判断.
5.unrecognized selector
简易示例代码:
- (void)testUnrecognizedSelectorCash {
[self performSelector:@selector(testSelCrash) withObject:nil afterDelay:0];
}
崩溃信息
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController testSelCrash]: unrecognized selector sent to instance 0x7f93a1e03e80'
一般出现Crash的情况:
- 对象被提前释放,指针变成野指针.
- 对象本身就是野指针.如声明一个局部对象,没有初始化就直接调用.
- 一个正常的对象调用一个不存在的方法.
- 给实体对象发送了不认识的消息,即对象调用方法出错.
预防方法:
runtime中具体的方法调用流程大致如下:
1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
3.如果没找到,去父类指针所指向的对象中执行1,2步骤.
4.以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制。
5.如果没有重写拦截调用的方法,程序报错。
由上图可见,在一个函数找不到时,runtime提供了三种方式去补救:
1、调用resolveInstanceMethod给机会让类添加这个实现这个函数
2、调用forwardingTargetForSelector让别的对象去执行这个函数
3、调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。
如果都不中,调用doesNotRecognizeSelector抛出异常。
通过实现NSObject的forwardingTargetForSelector:方法.并利用class_addMethod方法动态添加函数.
选择原因:
1.resolveInstanceMethod 需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的
- forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写
- forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写
可参考优秀文献:
6.非主线程刷UI
在非主线程刷UI将会导致app运行crash,现在Xcode在编译的时候会检测出当前有子线程操作UI有对应====的提示.但是其实也有必要对其进行处理。
预防方案处理
- 详细见Demo
以上为比较常见的简单崩溃和一些简单的预防方法,还有一些难以重现的崩溃.如野指针.内存泄露导致的崩溃等问题.
APP常见不好跟踪的异常崩溃:
- SIGSEGV || EXC_BAD_ACCESS || SIGBUS 野指针
- SIGPIPE 异常
- EXC_CRASH || SIGABRT 异常退出
- 内存泄露
1.EXC_BAD_ACCESS || SIGSEGV || SIGBUS 野指针
出现原因:
试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据.另外,在低内存的时候,也可能会产生这样的异常.
预防方法:
- 提高野指针Crash率.参考文献:
如何定位Obj-C野指针随机Crash(一):http://t.cn/R0NZLTU
如何定位Obj-C野指针随机Crash(二):http://t.cn/R0NZbXM
如何定位Obj-C野指针随机Crash(三):http://t.cn/R0NZqWM
2.SIGPIPE 异常
出现原因:
对一个端已经关闭的socket调用两次写入操作,第二次写入将会产生SIGPIPE信号,该信号默认结束进程。
预防方法:
// 仅在 IOS 系统上支持 SO_NOSIGPIPE
#if defined(SO_NOSIGPIPE) && !defined(MSG_NOSIGNAL)
// We do not want SIGPIPE if writing to socket.
const int value = 1;
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(int));
#endif
将改代码放在PCH文件中即可.
可参考文献:
1.http://t.cn/R0Cs9T0 (维基百科)
2.http://t.cn/R0CselE
3.http://t.cn/R0NvcIx
3.EXC_CRASH || SIGABRT 异常退出
出现原因:
程序异常退出,导致这类异常崩溃的原因是捕获到Objective-C/C++异常和调用了abort()函数,会在断言/app内部/操作系统用终止方法抛出.通常发生在异步执行系统方法的时候,如CoreData/NSUserDefaults等,还有一些其他的系统多线程操作.这并不一定意味着是系统代码存在bug,代码仅仅是成了无效状态,或者异常状态.
预防方法:
如果他们需要太多的时间来初始化,程序将被终止,因为触发了看门狗。如果是因为启动的时候被挂起,所产生的崩溃报告异常类型(Exception Subtype)将是launch_hang。因为扩展(extensions)并没有一个main函数,任何花销在初始化的时间都发生在静态构造函数(static constructors)和呈现在你的扩展(extensions)和依赖库的+load方法。你应该尽可能的延迟做这些工作。
4.内存泄露
出现原因:
程序运行时一直分配内存而不及时释放无用的内存,程序占用的内存越来越大,直到把系统分配给该APP的内存消耗殚尽,程序因无内存可用导致崩溃,这样的情况我们称之为内存泄漏。
预防方法:
1.使用Xcode自带的 Instruments 内存分析工具(Leaks)
2.使用WeRead团队提供的自动化工具来监测内存泄露问题
pod 'MLeaksFinder'
pod 'FBRetainCycleDetector
原理:为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针直接调用断言.
可参考文献:
MLeaksFinder:精准 iOS 内存泄露检测工具: http://t.cn/RGC7Gdg
结束语
该文的介绍不包含所有崩溃,只是抛砖引玉与大家一起讨论学习讨论.
为了少让我们少受Bug的摧残,还提心吊胆的担心线上某个崩溃问题引起大的影响。希望我们轻松工作,快乐生活,早日摆脱bug烦恼。