以前对于应用层的崩溃日志,没有主动收集过,只是下过应用沙盒下的日志,还有就是集成了bugly
来应对测试和线上的崩溃,最近是碰到一个需求,在sdk
里面上报应用层的崩溃日志,lz
就了解一下,与大家分享一下。
收集方式
Mach异常+Unix信号方式
Mach异常:Mach
异常是指最底层的内核级异常,被定义在 <mach/exception_types.h>
下 。
每个thread
,task
,host
都有一个异常端口数组,Mach
的部分API
暴露给了 用户态 ,用户态的开发者可以直接通过Mach API
设置thread
,task
,host
的异常端口,来捕获Mach
异常,抓取Crash
事件。
UNIX信号: 所有的Mach
异常都在host
层被转换成相应的UNIX
信号,并通过threadsignal
将信号投递到出错的线程。(iOS
中的 POSIX API
就是通过 Mach
之上的 BSD
层实现的。)
因此: EXC_BAD_ACCESS
(SIGSEGV
)表示的意思是:Mach
层的EXC_BAD_ACCESS
异常在host
层被转换成了SIGSEGV
信号,并投递到出错的线程。
Mach异常和UNIX信号都可以抓取crash事件,这两种方式哪个更好?
优选Mach
异常,因为Mach
异常处理会先于Unix
信号处理发生,如果Mach
异常的handler
让程序exit
了,那么Unix
信号就永远不会到达这个进程了。
多个Crash日志收集服务的冲突
有时候为了防止与三方的一些日志收集服务的冲突,我们需要拿到之前的服务注册的handler,然后备份,等自己的回调函数处理完后再把之前备份的handler注册回去。
相关代码如下:
+ (void)defaultHandler {
//保存handler
previousHandler = NSGetUncaughtExceptionHandler();
//1.捕获一些异常导致的崩溃
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
// 2.捕获非异常情况,通过signal传递出来的崩溃
signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);
}
应用级的异常NSException
// 崩溃时的回调函数
void UncaughtExceptionHandler(NSException * exception) {
// 获取异常的堆栈信息
NSArray *callStack = [exception callStackSymbols];
//获取异常的名称
NSString *exceptionName = [exception name];
//获取异常的原因
NSString *excepReason = [exception reason];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setObject:callStack forKey:kCaughtExceptionStackInfoKey];
NSException *customException = [NSException exceptionWithName:exceptionName reason:excepReason userInfo:userInfo];
[CatchCrash performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
//将异常塞给之前的第三方
if (previousHandler) {
previousHandler(exception);
}
}
通过系统的函数我们很容易就能获取到异常的堆栈信息,原因,名称,例如我们常见的 数组越界,插入空的数据,未实现的方法等都会走这个方法,lz
在下面的demo
里面也写的例子演示
Unix信号
void SignalHandler(int signal)
{
// 这种情况的崩溃信息,就另某他法来捕获吧
NSArray *callStack = [CatchCrash backtrace];
NSException *customException = [NSException exceptionWithName:kSignalExceptionName
reason:[NSString stringWithFormat:NSLocalizedString(@"Signal %d was raised.", nil),signal]
userInfo:@{kSignalKey:[NSNumber numberWithInt:signal]}];
[CatchCrash performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
+ (NSArray *)backtrace
{
void* callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
for (int i = 0; i < frames; i++) {
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
return backtrace;
}
关于Unix信号方式方式捕获的异常,通常见于EXC_BAD_ACESS
野指针错误,demo
里面也有例子
关于signal信号的捕捉,在
Xcode
调试时,Debugger
模式会先于我们的代码catch
到所有的crash
想看看到此类崩溃,demo
不能连着xcode
,不然看不到
对于搜集到的崩溃日志的处理
+ (void)handleException:(NSException *)exception
{
NSString *exceptionInfo = [NSString stringWithFormat:@"========异常错误报告========:\n%@\n%@\n%@",[exception name],[exception reason],[[exception userInfo] objectForKey:kCaughtExceptionStackInfoKey]];
NSString * path = [applicationDocumentsDirectory() stringByAppendingPathComponent:@"Exception.txt"];
// 将一个txt文件写入沙盒
[exceptionInfo writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
Mach
异常+Unix
信号方式产生的崩溃,lz
放到了沙盒下面,在下一次程序启动的时候就上传,上传完成之后就移除沙盒下面的文件
本地日志解析
在本地新建一个项目,写一个简单的崩溃程序
NSString *str = nil;
NSDictionary *dict = @{@"test":str};
然后用Xcode
打包,把生成的ipa
上传到蒲公英,然后下载到手机,点开运行一下,桌面创建一个文件夹crash
-
获取crash文件
Xcode->Window->Devices and Simulators->View Device Logs->右键导出crash文件。 -
获取dSYM文件
Window->Organizer->.xcarchive->右键显示包内容->dSYMs文件->xxx.app.dSYM -
symbolicatecrash工具
通过find找到symbolicatecrash工具的路径
find /Applications/Xcode.app -name symbolicatecrash -type f
前往文件夹->粘贴路径->symbolicatecrash工具
把crash文件+dSYM文件+symbolicatecrash工具copy到刚才创建的crash文件
cd到crash文件,执行命令
./symbolicatecrash ./*.crash ./*.app.dSYM > symbol.crash
如果出现 Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 69
执行命令
export DEVELOPER_DIR=/Applications/XCode.app/Contents/Developer
然后在执行
./symbolicatecrash ./*.crash ./*.app.dSYM > symbol.crash
可以看到本地生成的日志分析文件symbol.crash文件
Application Specific Information:
abort() called
Last Exception Backtrace:
0 CoreFoundation 0x1a067d27c __exceptionPreprocess + 228
1 libobjc.A.dylib 0x19f8579f8 objc_exception_throw + 55
2 CoreFoundation 0x1a05f6ce8 _CFThrowFormattedException + 111
3 CoreFoundation 0x1a057e9a8 -[__NSPlaceholderDictionary initWithObjects:forKeys:count:] + 351
4 CoreFoundation 0x1a056f584 +[NSDictionary dictionaryWithObjects:forKeys:count:] + 63
5 testaaa 0x102a5a548 _hidden#0_ + 25928 (__hidden#4_:22)
6 UIKitCore 0x1cca0dfc8 -[UIViewController loadViewIfRequired] + 1011
7 UIKitCore 0x1cca0e3cc -[UIViewController view] + 27
8 UIKitCore 0x1ccfecba4 -[UIWindow addRootViewControllerViewIfPossible] + 135
9 UIKitCore 0x1ccfed14c -[UIWindow _setHidden:forced:] + 271
10 UIKitCore 0x1ccffda28 -[UIWindow makeKeyAndVisible] + 47
11 UIKitCore 0x1ccfb0648 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 3531
12 UIKitCore 0x1ccfb5d20 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1539
13 UIKitCore 0x1cc8792dc __111-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:]_block_invoke + 775
14 UIKitCore 0x1cc881874 +[_UICanvas _enqueuePostSettingUpdateTransactionBlock:] + 159
15 UIKitCore 0x1cc878f60 -[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:] + 235
16 UIKitCore 0x1cc879850 -[__UICanvasLifecycleMonitor_Compatability activateEventsOnly:withContext:completion:] + 1063
17 UIKitCore 0x1cc877b9c __82-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:]_block_invoke + 743
18 UIKitCore 0x1cc877864 -[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:] + 427
19 UIKitCore 0x1cc87c3a4 __125-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke + 219
20 UIKitCore 0x1cc87d188 _performActionsWithDelayForTransitionContext + 111
21 UIKitCore 0x1cc87c25c -[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] + 243
22 UIKitCore 0x1cc880f5c -[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] + 359
23 UIKitCore 0x1ccfb4328 -[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 539
24 UIKitCore 0x1ccbb0ba8 -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 359
25 FrontBoardServices 0x1a2ff89fc -[FBSSceneImpl _didCreateWithTransitionContext:completion:] + 439
26 FrontBoardServices 0x1a300240c __56-[FBSWorkspace client:handleCreateScene:withCompletion:]_block_invoke_2 + 255
27 FrontBoardServices 0x1a3001c14 __40-[FBSWorkspace _performDelegateCallOut:]_block_invoke + 63
28 libdispatch.dylib 0x1a00bd7d4 _dispatch_client_callout + 15
29 libdispatch.dylib 0x1a00625d8 _dispatch_block_invoke_direct$VARIANT$mp + 223
30 FrontBoardServices 0x1a3033040 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 39
31 FrontBoardServices 0x1a3032cdc -[FBSSerialQueue _performNext] + 407
32 FrontBoardServices 0x1a3033294 -[FBSSerialQueue _performNextFromRunLoopSource] + 51
33 CoreFoundation 0x1a060f018 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 23
34 CoreFoundation 0x1a060ef98 __CFRunLoopDoSource0 + 87
35 CoreFoundation 0x1a060e880 __CFRunLoopDoSources0 + 175
36 CoreFoundation 0x1a06097bc __CFRunLoopRun + 1003
37 CoreFoundation 0x1a06090b0 CFRunLoopRunSpecific + 435
38 GraphicsServices 0x1a280979c GSEventRunModal + 103
39 UIKitCore 0x1ccfb7978 UIApplicationMain + 211
40 testaaa 0x102a5a5d4 main + 26068 (__hidden#7_:14)
41 libdyld.dylib 0x1a00ce8e0 start + 3
[__NSPlaceholderDictionary initWithObjects:forKeys:count:] + 351
[NSDictionary dictionaryWithObjects:forKeys:count:] + 63
从上面的日志解析文件可以很明显的看出是字典初始化插入数据为空,但是没有定位到具体的代码,这一点比较难受