项目可能用

关于iOS崩溃的认知和一些避免

2018-01-05  本文已影响995人  时过近迁
一个APP的崩溃率是对一个前端人员是否为合格的一个审核标准.
那么在每天日常中开发迭代中的那么多崩溃率信息有哪些呢?
又是因为什么原因导致的呢?该怎么去避免?

APP常见崩溃:

  1. Container crash(数组越界,插nil等)
  2. 字典的构造与修改
  3. 操作 UITableView UICollectionView 数据增删改读操作
  4. NSString crash (字符串操作的crash)
  5. unrecognized selector
  6. 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的情况:

崩溃信息

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0]'

以上预防方法:

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情况:

预防方法:

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 需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的

  1. forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写
  2. forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写
可参考优秀文献:

6.非主线程刷UI

在非主线程刷UI将会导致app运行crash,现在Xcode在编译的时候会检测出当前有子线程操作UI有对应====的提示.但是其实也有必要对其进行处理。

预防方案处理

以上为比较常见的简单崩溃和一些简单的预防方法,还有一些难以重现的崩溃.如野指针.内存泄露导致的崩溃等问题.

APP常见不好跟踪的异常崩溃:

  1. SIGSEGV || EXC_BAD_ACCESS || SIGBUS 野指针
  2. SIGPIPE 异常
  3. EXC_CRASH || SIGABRT 异常退出
  4. 内存泄露

1.EXC_BAD_ACCESS || SIGSEGV || SIGBUS 野指针

出现原因:

试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据.另外,在低内存的时候,也可能会产生这样的异常.


预防方法:

如何定位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烦恼。

上一篇下一篇

猜你喜欢

热点阅读