当前位置首页游戏解说《iOS Crash三部曲~之一Crash分类》

《iOS Crash三部曲~之一Crash分类》

类型:恐怖 枪战 剧情 美国 2010 

主演:张睿 石雪婧 赵亮 

导演:Bill Benz Jordan Kim 劳拉·墨菲 奥卡菲娜 

剧情简介

iOS Crash三部曲~之一Crash分类前言

移动应用崩溃主要是由操作系统引发,是指应用在运行过程中出现的强制关闭(Force Closing)现象,从而打断用户正在进行的操作体验。应用崩溃可以造成关键业务中断、用户留存率下降、品牌口碑变差、生命周期价值下降等影响。

那么为什么线下测试好好的app,上线之后就发生崩溃? 这其实是多线程操作、网络情况差异(弱网)、设备机型、操作习惯差异、等等,随着app用户数量增多,崩溃的问题也会逐渐多的暴露出来。

我们经常会和一些朋友或者产品在技术上切磋两招,就是“我们的崩溃率是百分之一”“我们的是万分之一”,崩溃率是衡量一个应用质量高低的基本指标,这一点是大家比较认可的。不过“万分之一”就一定比“百分之一”更好么?

其实要衡量一个指标,首先要统计计算口径,如果想评估崩溃造成的用户影响范围,我们可以去看UV崩溃率。

UV崩溃率 = 发生崩溃的UV / 登录的UV

早在几年前,行业内就有人提出,一个优秀的app的崩溃率iOS在0-3‰, Android在0-2‰。标准的范围iOS在3-8‰, Android在2-4‰。其实不管标准和优秀的定义到底如何,降低崩溃率是一个产品首要的任务,有效的控制崩溃率是必然不可少的环节。

1 crash产生来源

通常来说,crash产生来源于两种问题: 1、App代码逻辑BUG导致的 2、违反iOS系统规则导致的。具体如下图所示。

应用逻辑bug解释如下:

读写非法地址:主要是指:无效内存地址、空指针、未初始化指针、栈溢出、等尝试执行非法的指令,可能不被识别或者没有权限;逻辑错误:比如数学计算相关问题,除零操作;

违反iOS系统规则解释如下:

OOM:Out Of Memory:内存报警闪退

一般有两种:

a、系统整体内存使用较高,系统基于优先级杀死优先级较低的App

b、当前使用的App达到了 “high water mark”,也就是达到了系统对单个App的内存限制,系统会将Kill

OOMDetector是一个iOS内存监控组件,应用此组件可以帮助你轻松实现OOM监控、大内存分配监控、内存泄漏检测等功能。

当iOS检测到内存过低时,它的VM系统会发出低内存警告通知,尝试回收一些内存;如果情况没有得到足够的改善,iOS会终止后台应用以回收更多内存;

最后,如果内存还是不足,那么正在运行的应用可能会被终止掉。在Debug模式下,可以主动将客户端执行的动作逻辑写入一个log文件中,这样程序童鞋可以将内存预警的逻辑写入该log文件,当发生如下截图中的内存报警时,就是提醒当前客户端性能内存吃紧,可以通过Instruments工具中的Allocations和Leaks模块库来发现内存分配问题和内存泄漏问题。

Watchdog(响应超时)

当应用程序对一些特定的事件(比如启动、挂起、恢复、结束)响应不及时,苹果的Watchdog机制会把应用程序干掉,并生成一份相应的crash日志。

用户强制退出

一看到“用户强制退出”,首先可能想到的双击Home键,然后关闭应用程序。不过这种场景一般是不会产生crash日志的,因为双击Home键后,所有的应用程序都处于后台状态,而iOS随时都有可能关闭后台进程,当应用阻塞界面并停止响应时这种场景才会产生crash日志。这里指的“用户强制退出”场景,是稍微比较复杂点的操作:先按住电源键,直到出现“滑动关机”的界面时,再按住Home键,这时候当前应用程序会被终止掉,并且产生一份相应事件的crash日志。

2 crash分类

Crash的主要原因是应用收到了未处理的信号。未处理信号可能来源于三个地方: App本身、kernel(内核)、其他进程。因此crash异常也分为三种:NSException、Mach异常、Unix信号。

NSException:应用级异常,它是未被捕获的Objective-C异常,导致程序向自身发送了SIGABRT信号而崩溃,对于未捕获的Objective-C异常,是可以通过try catch来捕获的,或者通过NSSetUncaughtExceptionHandler()机制来捕获。

Mach异常:是指最底层的内核级异常。用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常。

Unix信号:又称BSD 信号,如果开发者没有捕获Mach异常,则会被host层的方法ux_exception()将异常转换为对应的UNIX信号,并通过方法threadsignal()将信号投递到出错线程。可以通过方法signal(x,SignalHandler)来捕获single。

2.1 NSException

Foundation下NSException掌控者程序的命运,程序的崩溃就是由NSException来控制的。一旦崩溃就会出现NSException信息。NSException exception一般情况下是跟try…catch…finally 联用的。如下图所示:

其中,@try里面是写正常需要执行的代码,有可能会出现异常,出现异常之后,会走@catch里面抛出异常,最终不管执行@try还是执行了@catch都会去执行@finally里面代码。

抛出异常之后,打印出来异常,可以看到崩溃相关的信息,如下图所示。

NSException使用场景?

示例1:自身封装SDK,若要保证SDK稳定且不崩溃,又要提示哪里出问题了,那么就可以使用NSException。由于面对的使用情况比较复杂。比如:

a:SDK提供的参数类型是NSString,但是开发者传了NSNumber类型.
b:方法随意调用也可能导致崩溃。
c:SDK内部对nil的处理不足导致的崩溃。

示例2:在数据请求回来数据之后,有各种的nil和null。如果加上try…catch代码就更容易捕获到服务器端的字段的null值,不至于引起崩溃。

优点:
可以防止APP崩溃,但是相应的代价是会占用一些系统资源。

2.2 Mach异常

主要有以下几种异常类型
1.访问一块坏内存[EXC_BAD_ACCESS] // SIGSEGV // SIGBUS]

2.异常退出[EXC_CRASH // SIGABRT]

3.追踪受限[EXC_BREAKPOINT // SIGTRAP]

4.非法指令[EXC_BAD_INSTRUCTION // SIGILL]

5.被保护的资源遭到侵害[EXC_GUARD]

6.资源限制[EXC_RESOURCE]

7.当要除零时EXC_ARITHMETIC

8.其他异常类型

其中EXC_BAD_ACCESS,此类型是最常见的crash,通常用于访问了不改访问的内存导致。一般EXC_BAD_ACCESS后面的"()"还会带有补充信息。EXC_BAD_INSTRUCTION,此类异常通常由于线程执行非法指令导致。EXC_ARITHMETIC,除零错误会抛出此类异常。

2.3 Unix信号

signal信号机制是属于计算机异常处理机制中的一种。 signal信号属于一种异步处理异常的机制之一。 类似于我们平常在命令行上对于死循环的程序,按下ctrl-z暂时挂起,ctrl-c程序终止,这些挂起,终止信号都属于signal信号的一种,常见的几种signal信号如下所示。

SIGABRT–程序中止命令中止信号SIGALRM–程序超时信号SIGFPE–程序浮点异常信号SIGILL–程序非法指令信号SIGHUP–程序终端中止信号SIGINT–程序键盘中断信号SIGKILL–程序结束接收中止信号SIGTERM–程序kill中止信号SIGSTOP–程序键盘中止信号SIGSEGV–程序无效内存中止信号SIGBUS–程序内存字节未对齐中止信号SIGPIPE–程序Socket发送失败中止信号

SIGABRT:是调用ABORT(中止信号)生成的信号。这是一个信号, SIG是所有unix信号的前缀名, ABRT是abort program的简称。当操作系统发现不安全的情况时,它能够对这种情况进行更多的控制;必要的话,它能要求进程进行清理工作。

通常UIKit框架在特定的前提条件没有满足或者一些其他情况出现时候调用C函数abort(由它来发送此信号)。当SIGABRT出现时,控制台通常会输出大量的信息,说明哪里出错。由于SIGABRT是可控制的,所有可以在控制台上输入bt命令打印出回溯信息。看门狗超时这种崩溃容易分辨,错误码固定是0x8badf00d,可以读作(Ate Bad Food)。在iOS中,经常出现在执行一个同步网络调用而阻塞主线程的情况。

SIGSEGV段错误信号(SIGSEGV)是操作系统产生的一个严重的问题,属于EXC_BAD_ACCESS的子类型,当硬件出现错误,访问不可读的内存地址或者向受保护的内存地址写入数据时,就会发生这个错误。这种错误并不常见,而导致这种错误最常见的原因是不正确的类型转换。

SIGBUS总线错误信号(SIGBUS)代表无效内存访问,即访问的内存是一个无效的内存地址。也就是说,那个地址指向的位置根本不是物理内存地址。和SIGSEGV一样,SIGBUS也属于EXC_BAD_ACCESS的子类型。

SIGILL代表SIGNAL ILLEGAL INSTRUCTION(非法指令信号)。当在处理器上执行非法指令时,它就会发生。执行非法指令是说,将函数指针传给另一个函数时,该函数指针由于某种原因是坏的,指向了一段已经释放了的内存或者一个数据段。

SIGSEGV通常由于重复释放对象导致,这种类型一般在ARC以后很少见到。

SIGABRT收到Abort信号退出,通常Foundation库中的容器为了保护状态正常会做一些检测,例如插入nil到数组中等会遇到此类错误。

野指针错误形式在Xcode中通常表现为:Thread 1:EXC_BAD_ACCESS(code=EXC_I386_GPFLT)错误。因为访问了一块已经不属于app的内存。

SEGV(Segmentation Violation),代表无效内存地址,比如空指针,未初始化指针,栈溢出等。

SIGBUS总线错误,与SIGSEGV不同的是,SIGSEGV访问的是无效地址,而SIGBUS访问的是有效地址,但总线访问异常(如地址对齐问题)。

SIGILL尝试执行非法的指令,可能不被识别或者没有权限。

SIGFPE数学计算相关问题,比如除零操作。

SIGIPIPE管道另一端没有进程接手数据。

归类总结之后,以下是所有的Unix信号。

程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP不能恢复至默认动作的信号有:SIGILL,SIGTRAP默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZl默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH3 Mach异常与Unix信号

XNU是由苹果电脑发展的操作系统内核,被使用于Mac OS X中。Mach是一个XNU的微内核核心,Mach异常是指最底层的内核级异常。

每个thread,task,host都有一个异常端口数组,Mach的部分API暴露给了用户态,用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常,抓取Crash事件。

所有Mach异常未处理,它将在host层被ux_exception转换为相应的Unix信号,Unix信号:又称BSD 信号,并通过threadsignal将信号投递到出错的线程。

iOS中的 POSIX API 就是通过 Mach 之上的 BSD 层实现的。

从 EXC_BAD_ACCESS (SIGSEGV) ,可以看出Mach层的EXC_BAD_ACCESS异常,在host层被转换成SIGSEGV信号投递到出错的线程。既然最终以信号的方式投递到出错的线程,那么就可以通过注册signalHandler来捕获信号:signal(SIGSEGV,signalHandler)。

这里延伸出来2个问题,大家思考以下:

1、捕获Mach异常或者Unix信号都可以抓到crash事件,这两种方式哪个更好呢?

答:优先选择Mach异常,因为Mach异常处理会先于Unix信号处理发生,如果Mach异常的handler让程序exit了,那么Unix信号就永远不会到达这个进程了。

2、如果优选Mach来捕获异常,为什么还要转化为unix信号呢?
答:转换Unix信号是为了兼容更为流行的POSIX标准(SUS规范),这样不必了解Mach内核也可以通过Unix信号的方式来兼容开发。

因为硬件产生的信号(通过CPU陷阱)被Mach层捕获,然后才转换为对应的Unix信号;苹果为了统一机制,于是操作系统和用户产生的信号(通过调用kill和pthread_kill)也首先沉下来被转换为Mach异常,再转换为Unix信号。也就是说整个流程是这样的:硬件产生信号或者kill或pthread_kill信号 --> Mach异常 --> Unix信号(SIGABRT)。如下图所示。

4 调试方法

几种常见的问题原因和调试方法。


4.1 调试技巧-1. signal SIGABRT

这在OC中经常出现,一般情况下,数组越界或者调用的方法不存在,会触发这个错误。

方法1,打开全局断点,错误一发生,Xcode会自动定位到错误的位置。

方法2,运用LLDB的调试指令,直接寻找引发错误的内存地址。

命令:image lookup --address 0x*********

4.2 调试技巧-2. EXC_BAD_ACCESS(Zombie Objects)

EXC_BAD_ACCESS,指向某块内存发送消息,但是该内存无法响应对应的消息指令。比如向一个已经释放的对象发送消息,就会报此错误。为了精确定位到到底是哪里的坏内存被访问了, 在Product -> Scheme -> Edict Scheme中, 勾选Zombie Objects。

4.3 调试技巧-3. Address Sanitizer

先来看一个示例。

明显90已经超出了原来的内存分配范围,仍然去赋值,程序会马上退出。在 malloc对象方面, Zombie Objects就很难起作用了。这个时候,我们可以打开Address Sanitizer运行应用。Address Sanitizer添加了额外的说明在内容接入当你编译代码的时候。随着应用的运行,Xcode将⚠️你假如内存以一种可能导致崩溃的方式接入。

Address Sanitizer:已经支持的错误检查包括:Use-after-free,:野指针问题, 访问已释放的内存区域Heap buffer overflow,:堆内存溢出Stack buffer overflow:栈内存溢出Global variable overflow:全局变量溢出Overflows in C++ containers : (Detects when a C++ container is accessed outside its bounds,字面意思:“在C++容器中的溢出”,实际表达的意思是:“矢量变量索引范围溢出检查”)Use-after-return:无效的栈上内存Use-after-scope:作用域外访问4.4 调试技巧-4. Memory Leak (内存泄露)

当希望某个对象释放掉的时候,它没有按照预想被释放掉,导致不必要的内存占用,比较常见的是Retain Circle(循环引用)。一起先来看一个示例。

//// TeacherVC.m// TestCrash//// Created by lmy on 2019/11/17.// Copyright © 2019 lmy. All rights reserved.//#import "TeacherVC.h"#import "Teacher.h"@interface TeacherVC ()@property(nonatomic,strong)Teacher * teacher;@property(nonatomic,copy)NSString * name;@end@implementation TeacherVC- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. [self.view setBackgroundColor:[UIColor blueColor]]; _teacher = [[Teacher alloc] init]; UIButton * backBut = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 80, 64)]; [backBut setBackgroundColor:[UIColor redColor]]; [backBut setTitle:@"返回" forState:UIControlStateNormal]; [self.view addSubview:backBut]; [backBut addTarget:self action:@selector(backButClick) forControlEvents:UIControlEventTouchUpInside]; UIButton * nameBut = [[UIButton alloc] initWithFrame:CGRectMake(100, 200, 100, 100)]; [nameBut setBackgroundColor:[UIColor redColor]]; [self.view addSubview:nameBut]; [nameBut addTarget:self action:@selector(nameButClick) forControlEvents:UIControlEventTouchUpInside];}- (void)backButClick{ [self dismissViewControllerAnimated:YES completion:nil];}- (void)nameButClick{ NSLog(@"点击了 nameButClick"); [self.teacher teach:^ { self.name = @"jack"; NSLog(@"self.name = jack"); }];}// dealloc- (void)dealloc{ NSLog(@"==== dealloc ====");}@end

推出新界面TeacherVC,执行nameButClick代码,再反回上一个页面,会发现本应该执行的dealloc方法并没有执行,说明TeacherVC这个控制器没有销毁,造成了内存泄露。

原因在于self强引用了teacher, teacher又强引用了一个block,而该block在回调时又调用了self,会导致该block又强引用了self,造成了一个保留环,最终导致self无法释放。形成了self -> teacher -> block -> self 的循环。

解决方法:

- (void)nameButClick{ NSLog(@"点击了 nameButClick");// [self.teacher teach:^ {// self.name = @"jack";// NSLog(@"self.name = jack");// }]; __weak typeof(self) weakSelf = self; [self.teacher teach:^{ typeof(weakSelf) strongSelf = weakSelf; strongSelf.name = @"jack"; NSLog(@"self.name = jack"); }];}

通过__weak修饰,在block回调里面用weakSelf,打破了闭环,目前是:self -> teacher -> block -> strongSelf-> weakSelf

再来看以下5中情况示例:

//情况一- (void)case1 { NSLog(@"case 1 Click"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.name = @"case 1"; });}//情况二- (void)case2 { NSLog(@"case 2 Click"); __weak typeof(self) weakSelf = self; [self.teacher teach:^{ typeof(weakSelf) strongSelf = weakSelf; strongSelf.name = @"case 2"; }];}//情况三- (void)case3 { NSLog(@"case 3 Click"); [self.teacher teach:^{ self.name = @"case 3"; }];}//情况四- (void)case4 { NSLog(@"case 4 Click"); [self.teacher teach:^{ self.name = @"case 4"; self.teacher = nil; }];}//情况五- (void)case5 { NSLog(@"case 5 Click"); Teacher *t = [[Teacher alloc] init]; [t teach:^{ self.name = @"case 5"; }];}
情况一:执行了dealloc,不泄露,此情况虽然是block,但未形成保留环block -> self情况二:执行了dealloc,不泄露,此情况就是内存泄漏后的一般处理了self ->teacher ->block ->strongSelf,后面那个strongSelf和原来的self并没有直接关系,因为strongSelf是通过weakSelf 得来的,而weakSelf又没有强引用原来的self情况三:未执行dealloc,内存泄漏,此情况就是最典型的循环引用了,形成保留环无法释放,self ->teacher ->block ->self情况四:执行了dealloc,不泄露,虽然也是保留环,但通过最后一句,使self不再强引用teacher,打破了保留环。(虽然这种写法可以防止内存泄漏,不过为了统一,还是建议最好还是按照情况二的写法。)情况五:执行了dealloc,不泄露,未形成保留环t ->block ->self

总结:除了上面一些自己来调试的技巧,现实项目中,我们经常会借助于一些第三方平台来抓取和分析崩溃报告。这些第三方平台有友盟、bugly、听云、talkingdata 等等……

5 Crash收集方式-UncaughtExceptionHandler

利用NSSetUncaughtExceptionHandler可以用来处理异常崩溃。崩溃报告系统会用NSSetUncaughtExceptionHandler方法设置全局的异常处理器。如果自定义NSSetUncaughtExceptionHandler监听事件,会导致第三方监听(如Bugly)失效,需要注意。

实现代码如下所示。

在.h文件中编写

//// QKUncaughtExceptionHandler.h// TestCrash//// Created by lmy on 2019/11/12.// Copyright © 2019 lmy. All rights reserved.//#import <Foundation/Foundation.h>@interface QKUncaughtExceptionHandler : NSObject+ (void)registerHandler;@end

在.m文件中编写

//// QKUncaughtExceptionHandler.m// TestCrash//// Created by lmy on 2019/11/12.// Copyright © 2019 lmy. All rights reserved.//#import "QKUncaughtExceptionHandler.h"//Caches路径#define QK_CachesPath [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]#define QK_Crash_Uncaught [NSString stringWithFormat:@"%@/Crash_Uncaught.data",QK_CachesPath]// 记录之前的崩溃回调函数static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;@implementation QKUncaughtExceptionHandler#pragma mark - Register+ (void)registerHandler { //将先前别人注册的handler取出并备份 previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler(); NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);}#pragma mark - Private// 崩溃时的回调函数static void UncaughtExceptionHandler(NSException * exception) { // 异常的堆栈信息 NSArray * stackArray = [exception callStackSymbols]; // 出现异常的原因 NSString * reason = [exception reason]; // 异常名称 NSString * name = [exception name]; NSString * exceptionInfo = [NSString stringWithFormat:@"uncaughtException异常错误报告==\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", name, reason, [stackArray componentsJoinedByString:@"\n"]]; NSLog(@"%@",exceptionInfo); // 保存崩溃日志到沙盒cache目录 NSError * error; [exceptionInfo writeToFile:QK_Crash_Uncaught atomically:YES encoding:NSUTF8StringEncoding error:&error]; //在自己handler处理完后自觉把别人的handler注册回去,规规矩矩的传递 if (previousUncaughtExceptionHandler) { previousUncaughtExceptionHandler(exception); } // 杀掉程序,这样可以防止同时抛出的SIGABRT被SignalException捕获 kill(getpid(), SIGKILL);}@end

应用级的异常NSException,需要特殊处理,需要去获取它的reason,name,callStackSymbols信息才能确定出问题的程序位置。通过读取NSExcption对象的callStackSymbols我们可以获取到的精准的调用信息。

大家来思考2个问题。

1、在自己的程序里集成多个Crash日志收集服务实是否是明智之举?
答:通常情况下,第三方功能性SDK都会集成一个Crash收集服务,以及时发现自己SDK的问题。当各家的服务都以保证自己的Crash统计正确完整为目的时,难免出现时序手脚,强行覆盖等等的恶意竞争,就会导致在其之前注册过的日志收集服务写出的Crash日志因为取不到NSException而丢失Last Exception Backtrace等信息。
如果有多个服务都通过NSSetUncaughtExceptionHandler注册了异常回调函数,如果在回调函数中直接退出,拒绝传递UncaughtExceptionHandler,那后面注册的回调函数就不会起作用。所以需要在注册异常处理函数时需要做两件事:

1、排查有哪些服务注册了回调函数,并做了不规范处理(即:在他的回调函数中直接退出了.借助fishHook等工具)。
2、拿到之前的服务注册的handler,然后备份,等自己的回调函数处理完后再把之前备份的handler注册回去。

因此,如果同时有多方通过NSSetUncaughtExceptionHandler注册异常处理程序,正确的作法是:后注册者通过NSGetUncaughtExceptionHandler将先前别人注册的handler取出并备份,在自己handler处理完后自觉把别人的handler注册回去,规规矩矩的传递。

2、未设置NSSetUncaughtExceptionHandler的NSException最后会转成Unix信号吗?
答:无论设置NSSetUncaughtExceptionHandler与否,只要未被try catch,最终都会被转成Unix信号,只不过设置了无法在其ExceptionHandler中无法获得最终发送的Unix信号类型

6 Crash收集方式-Unix信号

.h代码实现:

//// QKCrashSignalExceptionHandler.h// TestCrash//// Created by lmy on 2019/11/12.// Copyright © 2019 lmy. All rights reserved.//#import <Foundation/Foundation.h>@interface QKCrashSignalExceptionHandler : NSObject//注册方法+ (void)registerHandler;@end

.h定义一个注册方法registerHandler。

.m代码实现:

//// QKCrashSignalExceptionHandler.m// TestCrash//// Created by lmy on 2019/11/12.// Copyright © 2019 lmy. All rights reserved.//#import "QKCrashSignalExceptionHandler.h"#import <execinfo.h>//Caches路径#define QK_CachesPath [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]#define QK_Crash_Signal [NSString stringWithFormat:@"%@/Crash_Signal.data",QK_CachesPath]typedef void(*SignalHandler)(int signal, siginfo_t *info, void *context);static SignalHandler previousABRTSignalHandler = NULL;static SignalHandler previousBUSSignalHandler = NULL;static SignalHandler previousFPESignalHandler = NULL;static SignalHandler previousILLSignalHandler = NULL;static SignalHandler previousPIPESignalHandler = NULL;static SignalHandler previousSEGVSignalHandler = NULL;static SignalHandler previousSYSSignalHandler = NULL;static SignalHandler previousTRAPSignalHandler = NULL;@implementation QKCrashSignalExceptionHandler+ (void)registerHandler { NSLog(@"%@",QK_Crash_Signal); // 将先前别人注册的handler取出并备份 [self backupOriginalHandler]; //注册自己的 [self signalRegister];}#pragma mark - 备份handler+ (void)backupOriginalHandler { struct sigaction old_action_abrt; sigaction(SIGABRT, NULL, &old_action_abrt); if (old_action_abrt.sa_sigaction) { previousABRTSignalHandler = old_action_abrt.sa_sigaction; } struct sigaction old_action_bus; sigaction(SIGBUS, NULL, &old_action_bus); if (old_action_bus.sa_sigaction) { previousBUSSignalHandler = old_action_bus.sa_sigaction; } struct sigaction old_action_fpe; sigaction(SIGFPE, NULL, &old_action_fpe); if (old_action_fpe.sa_sigaction) { previousFPESignalHandler = old_action_fpe.sa_sigaction; } struct sigaction old_action_ill; sigaction(SIGILL, NULL, &old_action_ill); if (old_action_ill.sa_sigaction) { previousILLSignalHandler = old_action_ill.sa_sigaction; } struct sigaction old_action_pipe; sigaction(SIGPIPE, NULL, &old_action_pipe); if (old_action_pipe.sa_sigaction) { previousPIPESignalHandler = old_action_pipe.sa_sigaction; } struct sigaction old_action_segv; sigaction(SIGSEGV, NULL, &old_action_segv); if (old_action_segv.sa_sigaction) { previousSEGVSignalHandler = old_action_segv.sa_sigaction; } struct sigaction old_action_sys; sigaction(SIGSYS, NULL, &old_action_sys); if (old_action_sys.sa_sigaction) { previousSYSSignalHandler = old_action_sys.sa_sigaction; } struct sigaction old_action_trap; sigaction(SIGTRAP, NULL, &old_action_trap); if (old_action_trap.sa_sigaction) { previousTRAPSignalHandler = old_action_trap.sa_sigaction; }}#pragma mark - 注册方法+ (void)signalRegister { QKSignalRegister(SIGABRT); QKSignalRegister(SIGBUS); QKSignalRegister(SIGFPE); QKSignalRegister(SIGILL); QKSignalRegister(SIGPIPE); QKSignalRegister(SIGSEGV); QKSignalRegister(SIGSYS); QKSignalRegister(SIGTRAP);}#pragma mark Register Signalstatic void QKSignalRegister(int signal) { struct sigaction action; action.sa_sigaction = QKSignalHandler; action.sa_flags = SA_NODEFER | SA_SIGINFO; sigemptyset(&action.sa_mask); sigaction(signal, &action, 0);}#pragma mark SignalCrash Handlerstatic void QKSignalHandler(int signal, siginfo_t* info, void* context) { NSMutableString *mstr = [[NSMutableString alloc] init]; [mstr appendString:@"Signal Exception:\n"]; [mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised.\n", signalName(signal)]]; [mstr appendString:@"Call Stack:\n"]; // 这里过滤掉第一行日志 // 因为注册了信号崩溃回调方法,系统会来调用,将记录在调用堆栈上,因此此行日志需要过滤掉 for (NSUInteger index = 1; index < NSThread.callStackSymbols.count; index++) { NSString *str = [NSThread.callStackSymbols objectAtIndex:index]; [mstr appendString:[str stringByAppendingString:@"\n"]]; } [mstr appendString:@"threadInfo:\n"]; [mstr appendString:[[NSThread currentThread] description]]; NSLog(@"-=-=-=mstr=====>>>>> %@",mstr); // 保存崩溃日志到沙盒cache目录 NSError * error; [[NSString stringWithString:mstr] writeToFile:QK_Crash_Signal atomically:YES encoding:NSUTF8StringEncoding error:&error]; QKClearSignalRegister(); // 调用之前崩溃的回调函数 // 在自己handler处理完后自觉把别人的handler注册回去,规规矩矩的传递 previousSignalHandler(signal, info, context); kill(getpid(), SIGKILL);}#pragma mark Signal To Namestatic NSString *signalName(int signal) { NSString *signalName; switch (signal) { case SIGABRT: signalName = @"SIGABRT"; break; case SIGBUS: signalName = @"SIGBUS"; break; case SIGFPE: signalName = @"SIGFPE"; break; case SIGILL: signalName = @"SIGILL"; break; case SIGPIPE: signalName = @"SIGPIPE"; break; case SIGSEGV: signalName = @"SIGSEGV"; break; case SIGSYS: signalName = @"SIGSYS"; break; case SIGTRAP: signalName = @"SIGTRAP"; break; default: break; } return signalName;}#pragma mark Previous Signalstatic void previousSignalHandler(int signal, siginfo_t *info, void *context) { SignalHandler previousSignalHandler = NULL; switch (signal) { case SIGABRT: previousSignalHandler = previousABRTSignalHandler; break; case SIGBUS: previousSignalHandler = previousBUSSignalHandler; break; case SIGFPE: previousSignalHandler = previousFPESignalHandler; break; case SIGILL: previousSignalHandler = previousILLSignalHandler; break; case SIGPIPE: previousSignalHandler = previousPIPESignalHandler; break; case SIGSEGV: previousSignalHandler = previousSEGVSignalHandler; break; case SIGSYS: previousSignalHandler = previousSYSSignalHandler; break; case SIGTRAP: previousSignalHandler = previousTRAPSignalHandler; break; default: break; } if (previousSignalHandler) { previousSignalHandler(signal, info, context); }}#pragma mark Clearstatic void QKClearSignalRegister() { signal(SIGSEGV,SIG_DFL); signal(SIGFPE,SIG_DFL); signal(SIGBUS,SIG_DFL); signal(SIGTRAP,SIG_DFL); signal(SIGABRT,SIG_DFL); signal(SIGILL,SIG_DFL); signal(SIGPIPE,SIG_DFL); signal(SIGSYS,SIG_DFL);}@end

可以定义一个路径,QK_Crash_Signal 路径用来存储崩溃log。 每种signal信号注册一个静态变量,用来暂时保存别人注册的signal,注册方法

registerHandler里面1备份 2注册。

注册方法,每种signal都调用QKSignalRegister, 崩溃回调的方法:QKSignalHandler,QKClearSignalRegister 清除注册时,在自己handler处理完后自觉把别人的handler注册回去,规规矩矩的传递 previousSignalHandler(signal, info, context); 将信号,传给之前注册的。

在debug模式下,触发了崩溃,应用会直接崩溃到主函数,断点都没用,此时没有任何log信息显示出来,这是因为Xcode屏蔽了signal的回调。

如果想看log信息的话,我们需要在lldb中,输入以下命令,拿SIGABRT来说,在运行到断点位置后, 敲入命令:

pro hand -p true -s false SIGABRT

然后跳过断点,继续运行, 此时会触发 QKSignalHandler方法,输出完整的log信息。


相关文章:

《iOS Crash三部曲~之一Crash分类》

《iOS Crash三部曲~之二常见Crash》

《iOS Crash三部曲~之三Crash分析》

【神武4手游交易平台的相关新闻】

猜你喜欢

💟iOS Crash三部曲~之一Crash分类相关问题

1.请问哪个网站可以免费在线观看动漫《iOS Crash三部曲~之一Crash分类》?

优酷视频网友:

2.《iOS Crash三部曲~之一Crash分类》是什么时候上映/什么时候开播的?

腾讯视频网友:上映时间为2022年,详细日期可以去百度百科查一查。

3.《iOS Crash三部曲~之一Crash分类》是哪些演员主演的?

爱奇艺网友:iOS Crash三部曲~之一Crash分类演员表有,导演是。

4.动漫《iOS Crash三部曲~之一Crash分类》一共多少集?

电影吧网友:目前已更新到全集已完结

5.手机免费在线点播《iOS Crash三部曲~之一Crash分类》有哪些网站?

手机电影网网友:美剧网、腾讯视频、电影网

6.《iOS Crash三部曲~之一Crash分类》评价怎么样?

百度最佳答案:《iOS Crash三部曲~之一Crash分类》口碑不错,演员阵容强大演技炸裂,并且演员的演技一直在线,全程无尿点。你也可以登录百度问答获得更多评价。

  • iOS Crash三部曲~之一Crash分类百度百科 iOS Crash三部曲~之一Crash分类版原著 iOS Crash三部曲~之一Crash分类什么时候播 iOS Crash三部曲~之一Crash分类在线免费观看 iOS Crash三部曲~之一Crash分类演员表 iOS Crash三部曲~之一Crash分类大结局 iOS Crash三部曲~之一Crash分类说的是什么 iOS Crash三部曲~之一Crash分类图片 在线iOS Crash三部曲~之一Crash分类好看吗 iOS Crash三部曲~之一Crash分类剧情介绍      iOS Crash三部曲~之一Crash分类角色介绍 iOS Crash三部曲~之一Crash分类上映时间 
  • Copyright © 2008-2018

    统计代码