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

一个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的情况:

  • 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]'

以上预防方法:
    1. 替换系统方法.通过 Method Swizzling 替换成自己的方法,然后再执行方法的时候加以判断.
    1. 使用类别扩展一个方法自定义实现对应操作后的预防.
    1. 万能基佬王:http://t.cn/R0CTokN
    1. 万能谷歌法: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的值.

预防方法:

    1. 检查参数和参数类型的判断.

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

    1. 对象被提前释放,指针变成野指针.
    1. 对象本身就是野指针.如声明一个局部对象,没有初始化就直接调用.
    1. 一个正常的对象调用一个不存在的方法.
    1. 给实体对象发送了不认识的消息,即对象调用方法出错.
预防方法:

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有对应====的提示.但是其实也有必要对其进行处理。

预防方案处理

  • 详细见Demo

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

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

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

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烦恼。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358