1、Crash产生原因

Crash我们不得不面对的问题,但是好多人在遇到Crash的时候都无从下手,很多的时候都是凭着感觉找问题。今天我做了5篇文章来帮助我们更加清晰的认清iOS中的Crash

想要了解更详细的内容可以点击这里

Crash分类

一般是由 Mach异常或 Objective-C 异常(NSException)引起的。我们可以针对这两种情况抓取对应的 Crash 事件

crash2.png
  • 1、Mach异常是最底层的内核级异常,如EXC_BAD_ACCESS(内存访问异常)
  • 2、Unix Signal是Unix系统中的一种异步通知机制,Mach异常在host层被ux_exception转换为相应的Unix Signal,并通过threadsignal将信号投递到出错的线程
  • 3、 NSException是OC层,由iOS库或者各种第三方库或Runtime验证出错误而抛出的异常。如NSRangeException(数组越界异常)
  • 4、当错误发生时候,先在最底层产生Mach异常;Mach异常在host层被转换为相应的Unix Signal; 在OC层如果有对应的NSException(OC异常),就转换成OC异常,OC异常可以在OC层得到处理;如果OC异常一直得不到处理,程序会强行发送SIGABRT信号中断程序。在OC层如果没有对应的NSException,就只能让Unix标准的signal机制来处理了。
  • 5、在捕获Crash事件时,优选Mach异常。因为Mach异常处理会先于Unix信号处理发生,如果Mach异常的handler让程序exit了,那么Unix信号就永远不会到达这个进程了。而转换Unix信号是为了兼容更为流行的POSIX标准(SUS规范),这样就不必了解Mach内核也可以通过Unix信号的方式来兼容开发

Mach异常

Mach操作系统微内核,是许多新操作系统的设计基础。Mach微内核中有几个基础概念:

  • Tasks,拥有一组系统资源的对象,允许"thread"在其中执行。
  • Threads,执行的基本单位,拥有task的上下文,并共享其资源。
  • Ports,task之间通讯的一组受保护的消息队列;task可对任何port发送/接收数据。
  • Message,有类型的数据对象集合,只可以发送到port。

Mach 异常是指最底层的内核级异常,被定义在 <mach/exception_types.h>下。mach异常由处理器陷阱引发,在异常发生后会被异常处理程序转换成Mach消息,接着依次投递到thread、task和host端口。如果没有一个端口处理这个异常并返回KERN_SUCCESS,那么应用将被终止。每个端口拥有一个异常端口数组,系统暴露了后缀为_set_exception_ports的多个API让我们注册对应的异常处理到端口中

Mach异常方式

crash.png

Mach提供少量API

// 内核中创建一个消息队列,获取对应的port
mach_port_allocate();
// 授予task对port的指定权限
mach_port_insert_right();
// 通过设定参数:MACH_RSV_MSG/MACH_SEND_MSG用于接收/发送mach message
mach_msg();

Mach异常捕获
task_set_exception_ports(),设置内核接收Mach异常消息的Port,替换为自定义的Port后,即可捕获程序执行过程中产生的异常消息。

+ (void)createAndSetExceptionPort {
mach_port_t server_port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
assert(kr == KERN_SUCCESS);
NSLog(@"create a port: %d", server_port);

kr = mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND);
assert(kr == KERN_SUCCESS);

kr = task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS | EXC_MASK_CRASH, server_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE);

[self setMachPortListener:server_port];
}

// 构造BAD MEM ACCESS Crash
- (void)makeCrash {
NSLog(@"********** Make a [BAD MEM ACCESS] now. **********");
*((int *)(0x1234)) = 122;
}

以上代码参考iOS Mach异常和signal信号

mach异常即便注册了对应的处理,也不会导致影响原有的投递流程。此外,即便不去注册mach异常的处理,最终经过一系列的处理,mach异常会被转换成对应的UNIX信号,一种mach异常对应了一个或者多个信号类型。因此在捕获crash要提防二次采集的可能

crash3.png

处理signal

当错误发生时候,先在最底层产生Mach异常;Mach异常在host层被转换为相应的Unix Signal; 在OC层如果有对应的NSException(OC异常),就转换成OC异常,OC异常可以在OC层得到处理;如果OC异常一直得不到处理,程序会强行发送SIGABRT信号中断程序。在OC层如果没有对应的NSException,就只能让Unix标准的signal机制来处理了

signal.h中声明了32种异常信号,常见的有以下几种

  • 1、SIGILL 执行了非法指令,一般是可执行文件出现了错误
  • 2、SIGTRAP 断点指令或者其他trap指令产生
  • 3、SIGABRT 调用abort产生
  • 4、SIGBUS 非法地址。比如错误的内存类型访问、内存地址对齐等
  • 5、SIGSEGV 非法地址。访问未分配内存、写入没有写权限的内存等
  • 6、SIGFPE 致命的算术运算。比如数值溢出、NaN数值等

应用
1.AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.

InstallSignalHandler();//信号量截断
InstallUncaughtExceptionHandler();//系统异常捕获

return YES;
}

2.SignalHandler.m的实现

void SignalExceptionHandler(int signal)
{
NSMutableString *mstr = [[NSMutableString alloc] init];
[mstr appendString:@"Stack:\n"];
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i <frames; ++i) {
[mstr appendFormat:@"%s\n", strs[i]];
}
[SignalHandler saveCreash:mstr];

}

void InstallSignalHandler(void)
{
signal(SIGHUP, SignalExceptionHandler);
signal(SIGINT, SignalExceptionHandler);
signal(SIGQUIT, SignalExceptionHandler);

signal(SIGABRT, SignalExceptionHandler);
signal(SIGILL, SignalExceptionHandler);
signal(SIGSEGV, SignalExceptionHandler);
signal(SIGFPE, SignalExceptionHandler);
signal(SIGBUS, SignalExceptionHandler);
signal(SIGPIPE, SignalExceptionHandler);
}

有关错误类型可以看上面的说明,SignalExceptionHandler是信号出错时候的回调。当有信号出错的时候,可以回调到这个方法

3.UncaughtExceptionHandler.m的实现

void HandleException(NSException *exception)
{
// 异常的堆栈信息
NSArray *stackArray = [exception callStackSymbols];
// 出现异常的原因
NSString *reason = [exception reason];
// 异常名称
NSString *name = [exception name];
NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
NSLog(@"%@", exceptionInfo);
[UncaughtExceptionHandler saveCreash:exceptionInfo];
}

void InstallUncaughtExceptionHandler(void)
{
NSSetUncaughtExceptionHandler(&HandleException);
}

代码参考至向晨宇的技术博客-iOS异常捕获

demo地址

NSException异常

常见的NSException异常有

  • 1、unrecognized selector crash
  • 2、KVO crash
  • 3、NSNotification crash
  • 4、NSTimer crash
  • 5、Container crash(数组越界,插nil等)
  • 6、NSString crash (字符串操作的crash)
  • 7、Bad Access crash (野指针)
  • 8、UI not on Main Thread Crash (非主线程刷UI(机制待改善))

更加详细的信息请参考Baymax:网易iOS App运行时Crash自动防护实践

unrecognized selector类型

unrecognized selector类型的crash在app众多的crash类型中占着比较大的成分,通常是因为一个对象调用了一个不属于它方法的方法导致的。

方法调用流程
runtime中具体的方法调用流程大致如下:

  • 1、在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
  • 2、如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
  • 3、如果没找到,去父类指针所指向的对象中执行1,2.
  • 4、以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制。
  • 5、如果没有重写拦截调用的方法,程序报错。

在一个函数找不到时,runtime提供了三种方式去补救:

  • 1、调用resolveInstanceMethod给个机会让类添加这个实现这个函数
  • 2、调用forwardingTargetForSelector让别的对象去执行这个函数
  • 3、调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。

通过重写NSObject的forwardingTargetForSelector方法,我们就可以将无法识别的方法进行拦截并且将消息转发到安全的桩类对象中,从而可以使app继续正常运行

KVO crash 产生原因

KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受收到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。

KVO机制在iOS的很多开发场景中都会被使用到。不过如果一不小心使用不当的话,会导致大量的crash问题

通过会导致KVO Crash的两种情形

  • 1、KVO的被观察者dealloc时仍然注册着KVO导致的crash
  • 2、添加KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)导致的crash

解决方法:可以让被观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张map来维护KVO整个关系。具体就是使用runTime的交换方法重写KVO的一些方法

NSNotification类型crash防护

当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。
NSNotification类型的crash多产生于程序员写代码时候犯疏忽,在NSNotificationCenter添加一个对象为observer之后,忘记了在对象dealloc的时候移除它。
所幸的是,苹果在iOS9之后专门针对于这种情况做了处理,所以在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。
不过针对于iOS9之前的用户,我们还是有必要做一下NSNotification Crash的防护的。

NSNotification Crash的防护原理很简单, 利用method swizzling hook NSObject的dealloc函数,再对象真正dealloc之前先调用一下[[NSNotificationCenter defaultCenter] removeObserver:self]即可。

NSTimer类型crash防护

在程序开发过程中,大家会经常使用定时任务,但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:接口做重复性的定时任务时存在一个问题:NSTimer会强引用target实例,所以需要在合适的时机invalidate定时器,否则就会由于定时器timer强引用target的关系导致target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash。 crash的展现形式和具体的target执行的selector有关。

与此同时,如果NSTimer是无限重复的执行一个任务的话,也有可能导致target的selector一直被重复调用且处于无效状态,对app的CPU,内存等性能方面均是没有必要的浪费。

那么解决NSTimer的问题的关键点在于以下两点:

  • 1、NSTimer对其target是否可以不强引用
  • 2、是否找到一个合适的时机,在确定NSTimer已经失效的情况下,让NSTimer自动invalidate

Container crash 防护方案

Container crash 类型的防护方案也比较简单,针对于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些常用的会导致崩溃的API进行method swizzling,然后在swizzle的新方法中加入一些条件限制和判断,从而让这些API变的安全

野指针crash 防护方案

野指针问题的解决思路方向其实很容易确定,XCode提供了Zombie的机制来排查野指针的问题,那么我们这边可以实现一个类似于Zombie的机制,加上对zombie实例的全部方法拦截机制 和 消息转发机制,那么就可以做到在野指针访问时不Crash而只是crash时相关的信息。
同时还需要注意一点:因为zombie的机制需要在对象释放时保留其指针和相关内存占用,随着app的进行,越来越多的对象被创建和释放,这会导致内存占用越来越大,这样显然对于一个正常运行的app的性能有影响。所以需要一个合适的zombie对象释放机制,确定zombie机制对内存的影响是有限度的

非主线程刷UI类型crash防护

在非主线程刷UI将会导致app运行crash,有必要对其进行处理。
目前初步的处理方案是swizzle UIView类的以下三个方法:

- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;

在这三个方法调用的时候判断一下当前的线程,如果不是主线程的话,直接利用 dispatch_async(dispatch_get_main_queue(), ^{ //调用原本方法 });
来将对应的刷UI的操作转移到主线程上,同时统计错误信息。
但是真正实施了之后,发现这三个方法并不能完全覆盖UIView相关的所有刷UI到操作,但是如果要将全部到UIView的刷UI的方法统计起来并且swizzle,感觉略笨拙而且不高效。

漫谈 iOS Crash 收集框架

全面的理解和分析iOS的崩溃日志

iOS实录14:浅谈iOS Crash(一)

质量监控-保护你的crash

深入iOS系统底层之crash解决方法介绍

Baymax:网易iOS App运行时Crash自动防护实践

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

推荐阅读更多精彩内容

  • [这是第14篇] 序: iOS Crash问题是iOS开发中难以忽视的存在,本文就捕获iOS Crash、Cras...
    南华coder阅读 9,849评论 21 116
  • 一、前言 在日常开发中或者测试过程中,我们的应用可能会出现Crash的问题。对于这类问题我们要抱着零容忍的态度,因...
    WQ_UESTC阅读 5,703评论 4 39
  • 转载(漫谈 iOS Crash 收集框架) 前言 很早以前就和念茜认识,念茜不但技术功底扎实,而且长得很漂亮,说她...
    狂风无迹阅读 3,268评论 1 11
  • 本文就捕获iOS Crash、Crash日志组成、Crash日志符号化、异常信息解读、常见的Crash五部分介绍。...
    xukuangbo_阅读 1,569评论 0 0
  • 尽管我知道,在这个世上,我不能相信任何人,但是我就是义无反顾的爱上你,你是我生命中的希望,也是我最大的错误,这个世...
    浮梦依云阅读 126评论 0 1