项目中目前使用的crash日志统计工具是腾讯Bugly,但存在一个问题,Bugly当前版本不能绑定微信,出现crash时及时发送报警邮件,前端接入wehook有问题,于是我们决定自己捕获crash日志,上传服务器,实时监控线上版本发生的异常。
问题:
如何捕获App闪退的日志?怎么上传到服务器?
异常处理方式
异常处理可以分为两种
- 预先捕获异常并处理
- 未知异常发生后上报
预先捕获异常并处理
预先捕获异常就是在有可能出现异常的代码块里,可以创建异常对象并且手动抛出异常或者在执行可能出现的代码会自动触发并抛出异常,捕获到异常并执行处理措施。
这种异常我们都是使用标准的@try
@catch(id exception)
@finally
来处理的。
函数说明:
@try {
// 有可能出现异常的代码块,可以创建异常对象并且手动抛出异常或者在执行可能出现的代码会自动触发并抛出异常
} @catch (id exception) {
// 捕获到异常的处理措施
} @finally {
// 无论有无异常都要执行的操作,例如某些资源的释放等。
}
举个例子:
- (id)objForKey:(NSString *)key {
id ret = nil;
if ([self isKindOfClass:[NSArray class]])
{
if ([(NSArray*)self count]==1) {
self = [(NSArray*)self objectAtIndex:0];
} else {
return nil;
}
}
@try {
ret = [self valueForKey:key];
}
@catch (NSException *exception) {
ret = nil;
}
if (!ret || [(NSNull *)ret isEqual:[NSNull null]]) {
return nil;
} else {
return ret;
}
}
@try {
[[NSNotificationCenter defaultCenter] removeObserver:self];
} @catch (NSException *exception) {
NSLog(@"抛出异常");
} @finally {
}
@try
代码块中出现的异常,会发送到@catch
代码块中进行处理。这样出现的异常就不会将异常发送到系统级别,因此不会引起系统的闪退情况。
未知异常发生后上报
未知异常就是没有对可能发生异常的程序采取异常捕获机制,出现异常后引起App的闪退,也是今天要介绍的重点。这类异常的出现是由于没有对局部对异常进行处理,则系统默认将异常传递到系统级别。
举个例子:
NSMutableDictionary *dict =[NSMutableDictionary dictionary];
NSString *key=nil;
[dict setObj:@"value" forKey:key];
NSLog(@"dict=%@",dict);
执行抛出异常,终端查看crash日志如下:
Trapped uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSDictionaryM setObject:forKeyedSubscript:]: key cannot be nil'
(
0 CoreFoundation 0x00000001a4c317a8 50CF3336-313F-3A7D-9048-CB1ED8EC3368 + 1222568
1 libobjc.A.dylib 0x00000001a4953bcc objc_exception_throw + 56
2 CoreFoundation 0x00000001a4c872dc 50CF3336-313F-3A7D-9048-CB1ED8EC3368 + 1573596
3 CoreFoundation 0x00000001a4c90964 50CF3336-313F-3A7D-9048-CB1ED8EC3368 + 1612132
4 CoreFoundation 0x00000001a4b135c4 50CF3336-313F-3A7D-9048-CB1ED8EC3368 + 50628
5 HigoUtility 0x00000001038405e4 -[NSMutableDictionary(SafeAccess) setObj:forKey:] + 108
6 小着 0x00000001023a0908 -[HGProductViewController viewDidLoad] + 328
7 UIKitCore 0x00000001a8690750 BD57BD6E-12B4-3F92-85CA-754932DA499D + 4089680
8 UIKitCore 0x00000001a86951e0 BD57BD6E-12B4-3F92-85CA-754932DA499D + 4108768
9 UIKitCore 0x00000001a8607c10 BD57BD6E-12B4-3F92-85CA-754932DA499D + 3529744
10 UIKitCore 0x00000001a8607f30 BD57BD6E-12B4-3F92-85CA-754932DA499D + 3530544
11 UIKitCore 0x00000001a8608dd4 BD57BD6E-12B4-3F92-85CA-754932DA499D + 3534292
12 UIKitCore 0x00000001a860a0c4 BD57BD6E-12B4-3F92-85CA-754932DA499D + 3539140
13 UIKitCore 0x00000001a85ed624 BD57BD6E-12B4-3F92-85CA-754932DA499D + 3421732
14 UIKitCore 0x00000001a918c85c BD57BD6E-12B4-3F92-85CA-754932DA499D + 15607900
15 QuartzCore 0x00000001ab71f724 CF726782-41D5-39A6-8A87-AE03F4F07584 + 1382180
16 QuartzCore 0x00000001ab72587c CF726782-41D5-39A6-8A87-AE03F4F07584 + 1407100
17 QuartzCore 0x00000001ab7303c0 CF726782-41D5-39A6-8A87-AE03F4F07584 + 1450944
18 QuartzCore 0x00000001ab678f1c CF726782-41D5-39A6-8A87-AE03F4F07584 + 700188
19 QuartzCore 0x00000001ab6a28bc CF726782-41D5-39A6-8A87-AE03F4F07584 + 870588
20 UIKitCore 0x00000001a8cd1158 BD57BD6E-12B4-3F92-85CA-754932DA499D + 10645848
21 UIKitCore 0x00000001a8d74834 BD57BD6E-12B4-3F92-85CA-754932DA499D + 11315252
22 UIKitCore 0x00000001a8d6c1c8 BD57BD6E-12B4-3F92-85CA-754932DA499D + 11280840
23 CoreFoundation 0x00000001a4bafc18 50CF3336-313F-3A7D-9048-CB1ED8EC3368 + 691224
24 CoreFoundation 0x00000001a4bafb70 50CF3336-313F-3A7D-9048-CB1ED8EC3368 + 691056
25 CoreFoundation 0x00000001a4baf2f8 50CF3336-313F-3A7D-9048-CB1ED8EC3368 + 688888
26 CoreFoundation 0x00000001a4baa328 50CF3336-313F-3A7D-9048-CB1ED8EC3368 + 668456
27 CoreFoundation 0x00000001a4ba9ce8 CFRunLoopRunSpecific + 424
28 GraphicsServices 0x00000001aecf438c GSEventRunModal + 160
29 UIKitCore 0x00000001a8cd8444 UIApplicationMain + 1932
30 小着 0x000000010227c854 main + 148
31 libdyld.dylib 0x00000001a4a318f0 3D6D64B4-CB2B-3CC4-A7E9-02774DF7AE74 + 6384
)
对于没有提前设置捕获异常,或者项目中比较常见的因为数据异常导致的数组越界或字典key插入nil等,结果引起App闪退,这种异常发生后,系统会创建一个NSExcetion对象,并且在异常出抛出,等待有接受者,若没有传递给系统处理。
我们要做的就是在NSExcetion抛出异常时我们接收到crash日志并上报。
系统NSExcetion函数:
FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);
该函数是一个全局的函数,需要在AppDelegate的 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
里调用,这个函数是设置未知异常的捕获函数,参数是未知异常处理函数的函数名,该未知异常处理函数的模式如下:
typedef void NSUncaughtExceptionHandler(NSException *exception);
调用方式如下:
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
捕获到未知异常处理:
void uncaughtExceptionHandler(NSException *exception) {
// app退出前的一些处理任务,系统会等待该函数的执行完毕
NSArray *array = [exception callStackSymbols] ;//当前调用堆栈信息 NSString *reason = [exception reason] ; //得到崩溃的原因 NSString *name = [exception name];//异常类型 NSLog(@"exception type : %@ \n crash reason : %@ \n call stack info : %@", name, reason, arr);
}
调用该函数后如果再发生系统的未知异常的情况下,系统首先将异常传递个该函数,执行完该函数后App退出。因此,我们可以在这个函数内做一些业务处理,比如crash日志上报,但这个时候有一个问题需要注意:
我们不能在这个函数里执行异步操作,系统等待这个函数执行结束就会退出。
所以crash日志上报到服务器需要同步操作,而NSURLSession现在服务器请求都是异步操作,就会发生程序已经退出,将停止对数据的发送,因此我们不能使用AFNetWorking上报,解决的方式就是使用信号量机制,代码如下:
@implementation HGCrashLogManager
// 通过post方式来将异常信息发送到服务器
+ (void)sendCrashLog:(NSString *)crashLog {
dispatch_semaphore_t semophore = dispatch_semaphore_create(0); // 创建信号量
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:nil delegateQueue:nil];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kAPIjarvisRecordLogger]];
[request setHTTPMethod:@"POST"];
request.HTTPBody = [[NSString stringWithFormat:@"__crash=%@",crashLog]dataUsingEncoding:NSUTF8StringEncoding];
[[session dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"response%@",response);
dispatch_semaphore_signal(semophore); // 发送信号
}] resume];
dispatch_semaphore_wait(semophore, DISPATCH_TIME_FOREVER); // 等待
}
@end
在void uncaughtExceptionHandler(NSException *exception)里调用:
//捕获未知异常
void UncaughtExceptionHandler(NSException *exception) {
// 在app退出前的一些处理任务,系统会等待该函数的执行完毕
//异常信息
NSArray *callStack = [exception callStackSymbols];
NSString *reason = [exception reason];
NSString *name = [exception name];
//获取崩溃界面
NSString * nowview = NSStringFromClass([[HGFindVCManager currentViewController] class]);
NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:[DATACENTER staticParamForIMWithToken]];
NSMutableDictionary *dict =[NSMutableDictionary dictionary];
[dict setValue:nowview forKey:@"from_class"];
[dict setValue:name forKey:@"name"];
[dict setValue:reason forKey:@"reason"];
[dict setValue:callStack forKey:@"call_stack"];
[params setValue:dict forKey:@"ctx"];
DMLog(@"错误日志:%@",params);
//这里crash日志上报服务器需要同步操作,而NSURLSession现在服务器请求都是异步操作,就会发生程序已经退出,将停止对数据的发送,解决的方式就是使用信号量机制
[HGCrashLogManager sendCrashLog:[params jsonValue]];
}
这种方式验证crash日志是可以正常上传到服务器的,服务器收到日志会发出报警邮件并发送crash日志。当然最好的方式还是捕获到异常后我们做一些处理让App不再闪退,继续运行,后面再分享这种方式。