iOS开发:Crash异常总结与捕获

简介: 说到异常捕获,就必须要提到Crash问题,iOS中,Crash一般分为两种:1、一种是由EXC_BAD_ACCESS引起的,原因是访问了不属于本进程的内存地址,有可能是访问已被释放的内存;2、一种是未被捕获的目标C异常(NSException)记录,导致程序向自身发送了SIGABRT信号而崩溃。

在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。详见下图:

20180907132012852.png

20180907132031631.png


执行完成字后,打印的错误日志如下:


*** 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部分信号说明:


655218608429686460.jpg


在以上列出的信号中:


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】,如下图:


2018090714363361.png


其具体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”


#####参考博文:


1、Xcode里调试signal的信号回调处理函数;

2、iOS异常处理;


相关文章
|
6天前
|
安全 数据处理 Swift
深入探索iOS开发中的Swift语言特性
本文旨在为开发者提供对Swift语言在iOS平台开发的深度理解,涵盖从基础语法到高级特性的全面分析。通过具体案例和代码示例,揭示Swift如何简化编程过程、提高代码效率,并促进iOS应用的创新。文章不仅适合初学者作为入门指南,也适合有经验的开发者深化对Swift语言的认识。
24 9
|
6天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
4天前
|
iOS开发 开发者
探索iOS开发中的SwiftUI框架
【10月更文挑战第39天】在苹果的生态系统中,SwiftUI框架以其声明式语法和易用性成为开发者的新宠。本文将深入SwiftUI的核心概念,通过实际案例展示如何利用这一框架快速构建用户界面,并探讨其对iOS应用开发流程的影响。
|
7天前
|
JSON 前端开发 API
探索iOS开发之旅:打造你的第一个天气应用
【10月更文挑战第36天】在这篇文章中,我们将踏上一段激动人心的旅程,一起构建属于我们自己的iOS天气应用。通过这个实战项目,你将学习到如何从零开始搭建一个iOS应用,掌握基本的用户界面设计、网络请求处理以及数据解析等核心技能。无论你是编程新手还是希望扩展你的iOS开发技能,这个项目都将为你提供宝贵的实践经验。准备好了吗?让我们开始吧!
|
12天前
|
设计模式 前端开发 Swift
探索iOS开发:从初级到高级的旅程
【10月更文挑战第31天】在这篇文章中,我们将一起踏上iOS开发的旅程。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。我们将从基础开始,逐步深入到更高级的技术和概念。让我们一起探索iOS开发的世界吧!
|
14天前
|
设计模式 前端开发 Swift
探索iOS开发:从初级到高级的旅程
【10月更文挑战第28天】在这篇技术性文章中,我们将一起踏上一段探索iOS开发的旅程。无论你是刚入门的新手,还是希望提升技能的开发者,这篇文章都将为你提供宝贵的指导和灵感。我们将从基础概念开始,逐步深入到高级主题,如设计模式、性能优化等。通过阅读这篇文章,你将获得一个清晰的学习路径,帮助你在iOS开发领域不断成长。
46 2
|
20天前
|
安全 API Swift
探索iOS开发中的Swift语言之美
【10月更文挑战第23天】在数字时代的浪潮中,iOS开发如同一艘航船,而Swift语言则是推动这艘船前进的风帆。本文将带你领略Swift的独特魅力,从语法到设计哲学,再到实际应用案例,我们将一步步深入这个现代编程语言的世界。你将发现,Swift不仅仅是一种编程语言,它是苹果生态系统中的一个创新工具,它让iOS开发变得更加高效、安全和有趣。让我们一起启航,探索Swift的奥秘,感受编程的乐趣。
|
22天前
|
Swift iOS开发 开发者
探索iOS开发中的SwiftUI框架
【10月更文挑战第21天】在苹果生态系统中,SwiftUI的引入无疑为iOS应用开发带来了革命性的变化。本文将通过深入浅出的方式,带领读者了解SwiftUI的基本概念、核心优势以及如何在实际项目中运用这一框架。我们将从一个简单的例子开始,逐步深入到更复杂的应用场景,让初学者能够快速上手,同时也为有经验的开发者提供一些深度使用的技巧和策略。
45 1
|
10天前
|
存储 数据可视化 Swift
探索iOS开发之旅:从新手到专家
【10月更文挑战第33天】在这篇文章中,我们将一起踏上一场激动人心的iOS开发之旅。无论你是刚刚入门的新手,还是已经有一定经验的开发者,这篇文章都将为你提供宝贵的知识和技能。我们将从基础的iOS开发概念开始,逐步深入到更复杂的主题,如用户界面设计、数据存储和网络编程等。通过阅读这篇文章,你将获得成为一名优秀iOS开发者所需的全面技能和知识。让我们一起开始吧!
|
11天前
|
移动开发 Java Android开发
探索Android与iOS开发的差异性与互联性
【10月更文挑战第32天】在移动开发的大潮中,Android和iOS两大平台各领风骚。本文将深入浅出地探讨这两个平台的开发差异,并通过实际代码示例,展示如何在各自平台上实现相似的功能。我们将从开发环境、编程语言、用户界面设计、性能优化等多个角度进行对比分析,旨在为开发者提供跨平台开发的实用指南。
33 0