iOS Crash 闪退日志上传服务器

2,109 阅读9分钟

项目中目前使用的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不再闪退,继续运行,后面再分享这种方式。