异常捕获

线上APP获取崩溃信息的方式:

  • 使用系统 API 收集崩溃信息并上传到服务器。
  • 使用友盟、Bugly 等第三方收集分析SDK。
  • 使用 iTunes Connect 上的崩溃收集服务。

这里暂时只讲第一种。iOS提供了异常发生时的处理API,在程序启动的时候可以添加这样的Handler,这样程序发生异常的时候就可以对异常进行必要的处理。

1
void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);

下面的类就利用了系统提供的API来捕获出现的崩溃,并将异常信息存储到固定目录下的文件中,

1
2
3
4
5
6
7
8
//.h文件
#import <Foundation/Foundation.h>

@interface ExceptionHandler : NSObject

+ (void)setDefaultHandler;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//.m文件
#import "ExceptionHandler.h"

volatile int32_t UncaughtExceptionCount = 0; //当前处理的异常个数
volatile int32_t UncaughtExceptionMaximum = 10;//最大能够处理的异常个数

#pragma mark -文件目录
NSString *exceptionFilePath()
{
return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES) lastObject];
}

#pragma mark -收到异常通知时的回调函数
void UncaughtExceptionHandler(NSException *exception)
{
NSArray *arr = [exception callStackSymbols];
NSString *reason = [exception reason];
NSString *name = [exception name];

NSString *info = [NSString stringWithFormat:@"+异常崩溃报告+\nname:\n%@\nreason:\n%@\ncallStackSymbols:\n%@",name,reason,[arr componentsJoinedByString:@"\n"]];

NSString *path = [exceptionFilePath() stringByAppendingPathComponent:@"Exception.txt"];

[info writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
}

@implementation ExceptionHandler

#pragma mark -开始监听异常
+ (void)setDefaultHandler
{
NSSetUncaughtExceptionHandler (&UncaughtExceptionHandler);
}

调用代码:

1
2
3
4
5
6
7
8
9
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[ExceptionHandler setDefaultHandler];
NSArray *anArr = @[@(0)];
anArr[5];

return YES;
}

崩溃后得到日志信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+异常崩溃报告+
name:
NSRangeException
reason:
*** -[__NSSingleObjectArrayI objectAtIndex:]: index 5 beyond bounds [0 .. 0]
callStackSymbols:
0 CoreFoundation 0x00000001070dd1ab __exceptionPreprocess + 171
1 libobjc.A.dylib 0x0000000106772f41 objc_exception_throw + 48
2 CoreFoundation 0x000000010711d2df -[__NSSingleObjectArrayI objectAtIndex:] + 111
3 ASDF 0x0000000105e5a8b2 -[AppDelegate application:didFinishLaunchingWithOptions:] + 242
...
28 CoreFoundation 0x0000000107064b49 __CFRunLoopDoSources0 + 185
29 CoreFoundation 0x000000010706412f __CFRunLoopRun + 1279
30 CoreFoundation 0x00000001070639b9 CFRunLoopRunSpecific + 409
31 GraphicsServices 0x000000010c0ad9c6 GSEventRunModal + 62
32 UIKit 0x00000001075585e8 UIApplicationMain + 159
33 ASDF 0x0000000105e5bf4f main + 111
34 libdyld.dylib 0x000000010aa71d81 start + 1

但是,并不是所有的程序崩溃都是由于发生可以捕捉的异常的,有些时候引起崩溃的原因可能是:内存访问错误、重复释放等。上面的API对这些错误就无能为力了,因为这种错误它抛出的是Signal,所以必须要专门做Signal处理。

此时,可以使用下面的函数来注册signal异常时的回调函数:

1
signal(SIGABRT, signalExceptionHandler);

这样,当应用发生错误而产生上述signal后,就会进入我们自定义的回调函数signalExceptionHandler中。为了得到崩溃时的现场信息,还可以加入一些获取CallTrace的方法,完整代码如下:

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>

@interface ExceptionHandler : NSObject

+ (instancetype)shareInstance;
+ (NSArray *)backtrace;
+ (void)installExceptionHandler;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#import "ExceptionHandler.h"
#import <UIKit/UIKit.h>
#include <libkern/OSAtomic.h>
#include <execinfo.h>
#include <sys/signal.h>

//当前处理的异常个数
volatile int32_t UncaughtExceptionCount = 0;
//最大能够处理的异常个数
volatile int32_t UncaughtExceptionMaximum = 10;

const NSInteger ExceptionHandlerSkipAddressCount = 5;
const NSInteger ExceptionHandlerReportAddressCount = 10;

static ExceptionHandler *mExceptionHandler = nil;

#pragma mark -捕获信号后的回调函数
void signalExceptionHandler(int signo)
{
int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount);
if (exceptionCount > UncaughtExceptionMaximum){
return;
}
NSArray *callStack = [ExceptionHandler backtrace];

NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:
[NSNumber numberWithInt:signo] forKey:@"signal"];
[userInfo setValue:callStack forKey:@"callStack"];

//创建一个OC异常对象
NSException *ex = [NSException exceptionWithName:@"Name" reason:nil userInfo:userInfo];

//处理异常消息
[[ExceptionHandler shareInstance] performSelectorOnMainThread:@selector(onHandleSignalException:) withObject:ex waitUntilDone:YES];
}

@implementation ExceptionHandler

+ (instancetype)shareInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (mExceptionHandler == nil) {
mExceptionHandler = [[ExceptionHandler alloc] init];
}
});
return mExceptionHandler;
}

#pragma mark -注册异常处理回调
+ (void)installExceptionHandler
{
//注册程序由于abort()函数调用发生的程序中止信号
signal(SIGABRT, signalExceptionHandler);

//注册程序由于非法指令产生的程序中止信号
signal(SIGILL, signalExceptionHandler);

//注册程序由于无效内存的引用导致的程序中止信号
signal(SIGSEGV, signalExceptionHandler);

//注册程序由于浮点数异常导致的程序中止信号
signal(SIGFPE, signalExceptionHandler);

//注册程序由于内存地址未对齐导致的程序中止信号
signal(SIGBUS, signalExceptionHandler);

//程序通过端口发送消息失败导致的程序中止信号
signal(SIGPIPE, signalExceptionHandler);
}

#pragma mark -处理异常用到的方法
- (void)onHandleSignalException:(NSException *)exception
{
NSLog(@"++++出现崩溃:\n%@",exception.userInfo);

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);
}

#pragma mark -获取调用堆栈
+ (NSArray *)backtrace
{
void* callstack[128];

//该函数用于获取当前线程的调用堆栈
int frames = backtrace(callstack, 128);

//将从backtrace函数获取的信息转化为一个字符串数组
char **strs = backtrace_symbols(callstack, frames);

int i;

NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];

for (i = ExceptionHandlerSkipAddressCount;
i < ExceptionHandlerSkipAddressCount + ExceptionHandlerReportAddressCount; i++)
{
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
return backtrace;
}
@end

调用示例:

1
2
3
4
5
6
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[ExceptionHandler installExceptionHandler];
return YES;
}

这样,基本上所有崩溃都能捕获了。


异常捕获
https://davidlii.cn/2018/07/20/crash.html
作者
Davidli
发布于
2018年7月20日
许可协议