在APP用户量达到一定基数的时候,用户在使用应用的期间,或多或少的会碰到一些致使程序闪退的情况,而我们需要将这些情况收集起来。
一般情况下,应用程序发生闪退是,通常都会采用第三方平台进行统计分析,例如:
> * 1、友盟
> * 2、Flurry
> * 3、Crashlytics
而这篇博客讲的是如何利用苹果自身的sdk 【NSException】进行捕获收集这些闪退信息。
说到异常捕获,就必须要提到Crash问题,iOS中,Crash一般分为两种:
1、一种是由EXC_BAD_ACCESS引起的,原因是访问了不属于本进程的内存地址,有可能是访问已被释放的内存;
2、一种是未被捕获的目标C异常(NSException)记录,导致程序向自身发送了SIGABRT信号而崩溃。
遗憾的是,我们只能捕捉记录第二种方法,但如果我们日志记录得当,还是能够解决APP中绝大部分的崩溃问题,下面针对第二种方法做一些处理:
完整代码的下载地址:【DemoExceptionHandler】
一、系统崩溃
对于系统崩溃而引起的程序异常退出,可以通过【NSSetUncaughtExceptionHandler】机制捕获,这个方法比较简单。
二、处理signal
使用Obj-C的异常处理是得 不到signal的,如果要处理它,我们还要利用unix标准的signal机制,注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数。该函数中我们可以输出栈信息,版本信息等其他一切我们所想要的。NSSetUncaughtExceptionHandler 用来做异常处理,但功能非常有限.而引起崩溃的大多数原因如:内存访问错误,重复释放等错误就无能为力了,因为这种错误它抛出的是Signal,所以必须要专门做Signal处理。
针对以上问题,我阅读了解了多篇博客,借鉴了多家的集成,并对每个方法和属性做了注释,定义一个【UncaughtExceptionHandler】类,用来捕获处理所有的崩溃信息,方法如下:
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> @interface UncaughtExceptionHandler : NSObject /*! * 异常的处理方法 * * @param install 是否开启捕获异常 * @param showAlert 是否在发生异常时弹出alertView */ + (void)installUncaughtExceptionHandler:(BOOL)install showAlert:(BOOL)showAlert; @end
#import "UncaughtExceptionHandler.h" #include <libkern/OSAtomic.h> #include <execinfo.h> //NSException错误名称 NSString * const UncaughtExceptionHandlerSignalExceptionName = @"UncaughtExceptionHandlerSignalExceptionName"; //signal错误堆栈的条数 NSString * const UncaughtExceptionHandlerSignalKey = @"UncaughtExceptionHandlerSignalKey"; //错误堆栈信息 NSString * const UncaughtExceptionHandlerAddressesKey = @"UncaughtExceptionHandlerAddressesKey"; //初始化的错误条数 volatile int32_t UncaughtExceptionCount = 0; //错误最大的条数 const int32_t UncaughtExceptionMaximum = 10; ///是否弹窗提示 static BOOL showAlertView = nil; //异常处理 void HandleException(NSException *exception); //Signal类型错误信号处理 void SignalHandler(int signal); //获取app信息 NSString* getAppInfo(void); @interface UncaughtExceptionHandler() ///判断程序是否继续执行 @property (assign, nonatomic) BOOL dismissed; @end @implementation UncaughtExceptionHandler /*! * 1、异常的处理方法 * * @param install 是否开启捕获异常 * @param showAlert 是否在发生异常时弹出alertView */ + (void)installUncaughtExceptionHandler:(BOOL)install showAlert:(BOOL)showAlert { if (install && showAlert) { [[self alloc] alertView:showAlert]; } NSSetUncaughtExceptionHandler(install ? HandleException : NULL); signal(SIGABRT, install ? SignalHandler : SIG_DFL); signal(SIGILL, install ? SignalHandler : SIG_DFL); signal(SIGSEGV, install ? SignalHandler : SIG_DFL); signal(SIGFPE, install ? SignalHandler : SIG_DFL); signal(SIGBUS, install ? SignalHandler : SIG_DFL); signal(SIGPIPE, install ? SignalHandler : SIG_DFL); } ///设置是否弹窗提示 - (void)alertView:(BOOL)show { showAlertView = show; } #pragma mark - methond //med 1、专门针对Signal类型的错误获取堆栈信息 + (NSArray *)backtrace { //指针列表 void* callstack[128]; //backtrace用来获取当前线程的调用堆栈,获取的信息存放在这里的callstack中 //128用来指定当前的buffer中可以保存多少个void*元素 //返回值是实际获取的指针个数 int frames = backtrace(callstack, 128); //backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组 //返回一个指向字符串数组的指针 //每个字符串包含了一个相对于callstack中对应元素的可打印信息,包括函数名、偏移地址、实际返回地址 char **strs = backtrace_symbols(callstack, frames); int i; NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames]; for (i = 0; i < frames; i++) { [backtrace addObject:[NSString stringWithUTF8String:strs[i]]]; } free(strs); return backtrace; } //med 2、所有错误异常处理 - (void)handleException:(NSException *)exception { //验证和保存错误数据 [self validateAndSaveCriticalApplicationData:exception]; ///错误弹窗提示设置 if (!showAlertView) { return; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"出错啦" message:[NSString stringWithFormat:@"你可以尝试继续操作,但是应用可能无法正常运行.\n"] delegate:self cancelButtonTitle:@"退出" otherButtonTitles:@"继续", nil]; [alert show]; #pragma clang diagnostic pop CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop); while (!self.dismissed) { //点击继续 for (NSString *mode in (__bridge NSArray *)allModes) { //快速切换Mode CFRunLoopRunInMode((CFStringRef)mode, 0.001, false); } } //点击退出 CFRelease(allModes); NSSetUncaughtExceptionHandler(NULL); signal(SIGABRT, SIG_DFL); signal(SIGILL, SIG_DFL); signal(SIGSEGV, SIG_DFL); signal(SIGFPE, SIG_DFL); signal(SIGBUS, SIG_DFL); signal(SIGPIPE, SIG_DFL); if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName]) { kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]); } else { [exception raise]; } } //点击退出 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (void)alertView:(UIAlertView *)anAlertView clickedButtonAtIndex:(NSInteger)anIndex { #pragma clang diagnostic pop if (anIndex == 0) { self.dismissed = YES; } } //验证和保存错误数据 - (void)validateAndSaveCriticalApplicationData:(NSException *)exception { NSString *exceptionInfo = [NSString stringWithFormat:@"\n--------Log Exception---------\nappInfo :\n%@\n\nexception name :%@\nexception reason :%@\nexception userInfo :%@\ncallStackSymbols :%@\n\n--------End Log Exception-----", getAppInfo(),exception.name, exception.reason, exception.userInfo ? : @"no user info", [exception callStackSymbols]]; NSLog(@"%@", exceptionInfo); ///写入文件 // [exceptionInfo writeToFile:[NSString stringWithFormat:@"%@/Documents/error.log",NSHomeDirectory()] atomically:YES encoding:NSUTF8StringEncoding error:nil]; } @end ///2.1、奔溃异常处理 void HandleException(NSException *exception) { int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount); // 如果太多不用处理 if (exceptionCount > UncaughtExceptionMaximum) { return; } //获取调用堆栈 NSArray *callStack = [exception callStackSymbols]; NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]]; [userInfo setObject:callStack forKey:UncaughtExceptionHandlerAddressesKey]; //在主线程中,执行制定的方法, withObject是执行方法传入的参数 [[[UncaughtExceptionHandler alloc] init] performSelectorOnMainThread:@selector(handleException:) withObject: [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo] waitUntilDone:YES]; } //2.2、signal报错处理 void SignalHandler(int signal) { int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount); // 如果太多不用处理 if (exceptionCount > UncaughtExceptionMaximum) { return; } NSString* description = nil; switch (signal) { case SIGABRT: description = [NSString stringWithFormat:@"Signal SIGABRT was raised!\n"]; break; case SIGILL: description = [NSString stringWithFormat:@"Signal SIGILL was raised!\n"]; break; case SIGSEGV: description = [NSString stringWithFormat:@"Signal SIGSEGV was raised!\n"]; break; case SIGFPE: description = [NSString stringWithFormat:@"Signal SIGFPE was raised!\n"]; break; case SIGBUS: description = [NSString stringWithFormat:@"Signal SIGBUS was raised!\n"]; break; case SIGPIPE: description = [NSString stringWithFormat:@"Signal SIGPIPE was raised!\n"]; break; default: description = [NSString stringWithFormat:@"Signal %d was raised!",signal]; } NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; NSArray *callStack = [UncaughtExceptionHandler backtrace]; [userInfo setObject:callStack forKey:UncaughtExceptionHandlerAddressesKey]; [userInfo setObject:[NSNumber numberWithInt:signal] forKey:UncaughtExceptionHandlerSignalKey]; //在主线程中,执行指定的方法, withObject是执行方法传入的参数 [[[UncaughtExceptionHandler alloc] init] performSelectorOnMainThread:@selector(handleException:) withObject: [NSException exceptionWithName:UncaughtExceptionHandlerSignalExceptionName reason: description userInfo: userInfo] waitUntilDone:YES]; } ///获取app信息 NSString* getAppInfo() { NSString *appInfo = [NSString stringWithFormat:@"App : %@ %@(%@)\nDevice : %@\nOS Version : %@ %@\n", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"], [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], [UIDevice currentDevice].model, [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion]; return appInfo; }
最后,在didFinishLaunchingWithOptions中调用该函数
#import "UncaughtExceptionHandler.h" - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { //捕获异常 [UncaughtExceptionHandler installUncaughtExceptionHandler:YES showAlert:YES]; return YES; }
三、测试
以下是我的三个测试案例,前两个方法属于系统奔溃,直接编译运行,即可监听到Crash。
- (void)viewDidLoad { [super viewDidLoad]; // [self exceptionHandlerTest1]; // // [self exceptionHandlerTest2]; [self exceptionHandlerTest3]; } ///异常处理测试1 -(void)exceptionHandlerTest1{ //1、ios崩溃【数组越界】 NSArray *array= @[@"tom",@"xxx",@"ooo"]; [array objectAtIndex:5]; } ///异常处理测试2 -(void)exceptionHandlerTest2{ //2、用来制造一些异常【不存在string的方法】 [self performSelector:@selector(string) withObject:nil afterDelay:2.0]; } ///异常处理测试3 -(void)exceptionHandlerTest3{ //3、信号量 int list[2]={1,2}; int *p = list; //[奔溃位置]导致SIGABRT的错误,因为内存中根本就没有这个空间,哪来的free,就在栈中的对象而已 free(p); p[1] = 5; }
第三种测试属于signal类中的SIGABRT奔溃,在Xcode中测试的时候,程序不会进入bugrpt_signalHandler处理函数里面,因为Xcode屏蔽了signal的回调,为此,我们需要在【lldb】中输入以下命令,signal的回调才可以进来【pro hand -p true -s false SIGABRT】,其中,SIGABRT可以替换为你需要的任何signal类型,比如SIGSEGV。详见下图:
执行完成字后,打印的错误日志如下:
*** set a breakpoint in malloc_error_break to debug 2018-09-07 13:04:58.097353 DemoExceptionHandler[266:16655] --------Log Exception--------- appInfo : App : (null) 1.0(1) Device : iPhone OS Version : iOS 10.2 exception name :UncaughtExceptionHandlerSignalExceptionName exception reason :Signal SIGABRT was raised! exception userInfo :{ UncaughtExceptionHandlerAddressesKey = ( "0 DemoExceptionHandler 0x00000001000253dc +[UncaughtExceptionHandler backtrace] + 76", "1 DemoExceptionHandler 0x0000000100025174 SignalHandler + 676", "2 libsystem_platform.dylib 0x0000000182129338 _sigtramp + 36", "3 libsystem_pthread.dylib 0x000000018212f450 pthread_kill + 112", "4 libsystem_c.dylib 0x0000000181fdb400 abort + 140", "5 libsystem_malloc.dylib 0x000000018209d944 <redacted> + 0", "6 DemoExceptionHandler 0x0000000100024a50 -[ViewController exceptionHandlerTest3] + 96", "7 DemoExceptionHandler 0x00000001000248a4 -[ViewController viewDidLoad] + 92", "8 UIKit 0x0000000188f4e924 <redacted> + 1056", "9 UIKit 0x0000000188f4e4ec <redacted> + 28", "10 UIKit 0x0000000188f54c98 <redacted> + 76", "11 UIKit 0x0000000188f52138 <redacted> + 272", "12 UIKit 0x0000000188fc468c <redacted> + 48", "13 UIKit 0x00000001891d0cb8 <redacted> + 4068", "14 UIKit 0x00000001891d6808 <redacted> + 1656", "15 UIKit 0x00000001891eb104 <redacted> + 48", "16 UIKit 0x00000001891d37ec <redacted> + 168", "17 FrontBoardServices 0x0000000184c6f92c <redacted> + 36", "18 FrontBoardServices 0x0000000184c6f798 <redacted> + 176", "19 FrontBoardServices 0x0000000184c6fb40 <redacted> + 56", "20 CoreFoundation 0x0000000183046b5c <redacted> + 24", "21 CoreFoundation 0x00000001830464a4 <redacted> + 524", "22 CoreFoundation 0x00000001830440a4 <redacted> + 804", "23 CoreFoundation 0x0000000182f722b8 CFRunLoopRunSpecific + 444", "24 UIKit 0x0000000188fb97b0 <redacted> + 608", "25 UIKit 0x0000000188fb4534 UIApplicationMain + 208", "26 DemoExceptionHandler 0x0000000100025fec main + 124", "27 libdyld.dylib 0x0000000181f555b8 <redacted> + 4" ); UncaughtExceptionHandlerSignalKey = 6; } callStackSymbols :(null) --------End Log Exception----- 2018-09-07 13:04:58.280259 DemoExceptionHandler[266:16655] invalid mode 'kCFRunLoopCommonModes' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution.
四、Signal部分信号说明:
在以上列出的信号中:
1、程序不可捕获、阻塞或忽略的信号有:SIGKILL、SIGSTOP
2、不能恢复至默认动作的信号有:SIGILL、SIGTRAP
3、默认会导致进程流产的信号有:SIGABRT、SIGBUS、SIGFPE、SIGILL、SIGIOT、SIGQUIT、SIGSEGV、SIGTRAP、SIGXCPU、SIGXFSZ
4、默认会导致进程退出的信号有: SIGALRM、SIGHUP、SIGINT、SIGKILL、SIGPIPE、SIGPOLL、SIGPROF、SIGSYS、SIGTERM、SIGUSR1、SIGUSR2、SIGVTALRM
5、默认会导致进程停止的信号有:SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU
6、默认进程忽略的信号有:SIGCHLD、SIGPWR、SIGURG、SIGWINCH
7、此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;
8、SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。
要查询本机的信号表可以在终端输入【kill -l】,如下图:
其具体LinuxSignal列表,可以点击:【linux signal 列表】进一步了解。
五、Crash Callstack分析,属性说明:
1、 0x8badf00d : 在启动、终止应用或响应系统事件花费过长时间,意为“ate bad food”。
2、0xdeadfa11 : 用户强制退出,意为“dead fall”。(系统无响应时,用户按电源开关和HOME)
3、0xbaaaaaad : 用户按住Home键和音量键,获取当前内存状态,不代表崩溃
4、0xbad22222 : VoIP应用因为恢复得太频繁导致crash
5、0xc00010ff : 因为太烫了被干掉,意为“cool off”
6、0xdead10cc : 因为在后台时仍然占据系统资源(比如通讯录)被干掉,意为“dead lock”
#####参考博文:
2、iOS异常处理;