iOS收集Crash信息上报
在iOS开发中,最严重的bug估计就是应用奔溃,如果应用奔溃了,除了做好挨骂的准备,还需要冷静的下来去处理这个事情,接下来我们来看看需要做什么事情。
获取crash信息
我们首先第一个事情就是要知道应用的奔溃信息是什么,这里有几种方式去获取奔溃信息。
- 使用Bugly,友盟等第三方SDK登入后台查看奔溃信息
- 代码自动上传奔溃信息到服务器,然后通过恢复dSYM文件来查看奔溃信息
- 通过使用当前发生应用奔溃的设备导出相关的奔溃信息
- 如果是线上的应用,还可以通过itunesConnect来查看(非即时)
第一种集成第三方SDK的方案基本上不用我们管,只需要根据文档集成即可。
下面我们要讲的是第二种和第三种方案,第四种方案其实和第三种方案差不多,为什么要区分这两种方案呢,因为第二种方案中我们可以直接拿到奔溃的堆栈和具体信息,也就是可以看出在那段代码奔溃以及具体的奔溃内容。但是第三种和第四种方案我们拿到的奔溃信息是经过处理的奔溃信息,如下面所示:
Incident Identifier: 66BAB91F-07F7-4242-B8EF-8CC1771E5EF0
CrashReporter Key: bb6af27d5f29cc19a8df5dbdff702227fdb1232b
Hardware Model: iPhone8,1
Process: testImageSourceCode [25042]
Path: /private/var/containers/Bundle/Application/C8A371D9-3F19-4A17-A817-5FF35A40C8E7/testImageSourceCode.app/testImageSourceCode
Identifier: com.cmcc.enterprise-classID.onecardmultinumber.sdk
Version: 1 (1.0)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: com.cmcc.enterprise-classID.onecardmultinumber.sdk [8274]
Date/Time: 2018-08-22 15:35:09.3494 +0800
Launch Time: 2018-08-22 15:35:03.3154 +0800
OS Version: iPhone OS 11.3.1 (15E302)
Baseband Version: 4.56.00
Report Version: 104
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0
Application Specific Information:
abort() called
Filtered syslog:
None found
Last Exception Backtrace:
0 CoreFoundation 0x184152d8c __exceptionPreprocess + 228
1 libobjc.A.dylib 0x18330c5ec objc_exception_throw + 55
2 CoreFoundation 0x1840eb750 _CFThrowFormattedException + 111
3 CoreFoundation 0x18401b90c -[__NSArrayI objectAtIndex:] + 131
4 testImageSourceCode 0x100d55ee4 _hidden#0_ + 24292 (__hidden#4_:14)
5 testImageSourceCode 0x100d5ed6c _hidden#425_ + 60780 (__hidden#452_:131)
6 UIKit 0x18de826c8 -[UIApplication sendAction:to:from:forEvent:] + 95
7 UIKit 0x18dfa38a4 -[UIControl sendAction:to:forEvent:] + 79
8 UIKit 0x18de8877c -[UIControl _sendActionsForEvents:withEvent:] + 439
9 UIKit 0x18dfbe1dc -[UIControl touchesEnded:withEvent:] + 571
10 UIKit 0x18df05a48 -[UIWindow _sendTouchesForEvent:] + 2427
11 UIKit 0x18defa8f8 -[UIWindow sendEvent:] + 3159
12 UIKit 0x18def9238 -[UIApplication sendEvent:] + 339
13 UIKit 0x18e6dac0c __dispatchPreprocessedEventFromEventQueue + 2339
14 UIKit 0x18e6dd1b8 __handleEventQueueInternal + 4743
15 UIKit 0x18e6d6258 __handleHIDEventFetcherDrain + 151
16 CoreFoundation 0x1840fb404 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 23
17 CoreFoundation 0x1840fac2c __CFRunLoopDoSources0 + 275
18 CoreFoundation 0x1840f879c __CFRunLoopRun + 1203
19 CoreFoundation 0x184018da8 CFRunLoopRunSpecific + 551
20 GraphicsServices 0x185ffb020 GSEventRunModal + 99
21 UIKit 0x18dff978c UIApplicationMain + 235
22 testImageSourceCode 0x100d75ca8 main + 154792 (__hidden#956_:14)
23 libdyld.dylib 0x183aa9fc0 start + 3
看到上面的奔溃信息我们是一脸懵逼的,完全看不出是因为哪里的代码导致了错误,这个时候我们就要进行符号化,通过dSYM文件通过奔溃信息的地址找到源码中奔溃的地方。这个我们放到第三点来讲,下面我们先将第二个方案。
收集Crash信息
在iOS中,系统给我们提供了NSException
这个类来帮助我们收集异常信息。
NSException is used to implement exception handling and contains information about an exception — Apple Documentation.
我们可以看一下这个类里面都包含什么属性和方法:
@interface NSException : NSObject <NSCopying, NSCoding> {
@private
NSString *name;
NSString *reason;
NSDictionary *userInfo;
id reserved;
}
+ (NSException *)exceptionWithName:(NSExceptionName)name reason:(nullable NSString *)reason userInfo:(nullable NSDictionary *)userInfo;
- (instancetype)initWithName:(NSExceptionName)aName reason:(nullable NSString *)aReason userInfo:(nullable NSDictionary *)aUserInfo NS_DESIGNATED_INITIALIZER;
@property (readonly, copy) NSExceptionName name;
@property (nullable, readonly, copy) NSString *reason;
@property (nullable, readonly, copy) NSDictionary *userInfo;
@property (readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@property (readonly, copy) NSArray<NSString *> *callStackSymbols API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
- (void)raise;
@end
我们来看看几个比较重要的属性和方法:
例如我们碰到一个name: @"NSRangeException" - reason: @"*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 1]"
这样的错误
- name : exception的名字,在上面中就是NSRangeException
- reason : exception的原因,这是我们修复的最主要的提示。也就是上面的[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 1]。
- userInfo : 其他信息,一般用于自定义的时候传递一些其他的信息。
- callStackSymbols : 这个产生Exception的调用栈,从下到上。
- raise方法,这个方法就是让系统产生Exception,例如我们的APP如果检测到正在被其他不怀好意的人调试的时候,可以创建一个NSException的方法,然后调用raise直接闪退,不过他也有可能hook了这个方法,这里就不多说了。
既然我们知道了有这么一个类,那我们如何来捕捉系统异常呢,Crash分为两种,一种是由EXC_BAD_ACCESS引起的,原因是访问了不属于本进程的内存地址,有可能是访问已被释放的内存;另一种是未被捕获的Objective-C异常(NSException),导致程序向自身发送了SIGABRT信号而崩溃。其实对于未捕获的Objective-C异常,我们是有办法将它记录下来的。
我们先说第一种,第一种就是我们上面所说的Exception,系统提供了一个
FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);
方法来捕获异常,这个方法一般会在程序启动的时候就调用一次,这样才能保证捕获所有的异常。
在APPDelegate的didFinishLaunch方法中 调用 NSSetUncaughtExceptionHandler(&CrashExceptionHandler);
然后增加一个方法的实现。
void CrashExceptionHandler(NSException *exception){
NSArray *callStack = [exception callStackSymbols];
NSString *reson = [exception reason];
NSString *name = [exception name];
//TODO: 保存奔溃信息到本地,下次启动的时候上传到服务器
}
接下来我们来看看第二种异常,这种异常通过上面的方法无法捕捉,但是系统会发送一个信号,我们可以通过注册对应的Signal信息来监听是否捕捉到系统发出的异常信号。
同样是在APPDelaget中加入以下代码:
signal(SIGABRT, SignalExceptionHandler);
signal(SIGILL, SignalExceptionHandler);
signal(SIGSEGV, SignalExceptionHandler);
signal(SIGFPE, SignalExceptionHandler);
signal(SIGBUS, SignalExceptionHandler);
signal(SIGPIPE, SignalExceptionHandler);
然后实现 SignalExceptionHandler
方法
void SignalExceptionHandler(int signal){
NSArray *callStack = [LMExceptionHandler backtrace];
NSLog(@"信号捕获崩溃,堆栈信息:%@",callStack);
NSString *name = LMSignalException;
NSString *reason = [NSString stringWithFormat:@"signal %d was raised",signal];
//TODO: 保存信息上传到本地
}
+ (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;
}
通过以上的方法我们已经可以捕获到系统的crash信息,从而根据信息修复相关的bug。
通过Xcode查看Crash信息
如果奔溃发生在我们测试的设备上面,那么奔溃信息是保存到我们手机本地的,这个时候我们可以通过xcode来查看。
手机连接电脑,然后打开Xcode,点击Window->Devices And Simulators->左侧选择对应的设备,然后右侧点击View Device Logs,然后就可以看到以下的奔溃信息,你可以在搜索出对应的APP也可以根据时间来排序找到对应的那一条奔溃信息。
image如上图所示我们其实是看不出具体的错误信息以及堆栈信息的,根据这些我们很难再代码中找到导致奔溃的代码在哪里。接下来我们就要开始通过dSYM文件找到对应的堆栈。
dSYM
进行崩溃分析,首先要弄懂一个概念,就是符号集。
符号集是我们对ipa文件进行打包之后,和.app文件同级的后缀名为.dSYM的文件,这个文件必须使用Xcode进行打包才有。
每一个.dSYM文件都有一个UUID,和.app文件中的UUID对应,代表着是一个应用。而.dSYM文件中每一条崩溃信息也有一个单独的UUID,用来和程序的UUID进行校对。
我们如果不使用.dSYM文件获取到的崩溃信息都是不准确的。
符号集中存储着文件名、方法名、行号的信息,是和可执行文件的16进制函数地址对应的,通过分析崩溃的.Crash文件可以准确知道具体的崩溃信息。
我们每次Archive一个包之后,都会随之生成一个dSYM文件。每次发布一个版本,我们都需要备份这个文件,以方便以后的调试。进行崩溃信息符号化的时候,必须使用当前应用打包的电脑所生成的dSYM文件,其他电脑生成的文件可能会导致分析不准确的问题。
符号化crash信息
当程序崩溃的时候,我们可以获得到崩溃的错误堆栈,但是这个错误堆栈都是0x开头的16进制地址,需要我们使用Xcode自带的atos工具或者dSYMTools来将.Crash和.dSYM文件进行符号化,就可以得到详细崩溃的信息。
那我们如何得到dSYM文件呢
先打开Xcode,Windows->Organize->找到对应的app包,然后右键->Show in finder,找到appName. xcarchive->显示包内容->把dSYMs拷贝出来(或者就在里面操作)。
我们可以新建一个CrashFolder的文件夹,然后将上面的dSYMs文件拷贝到该文件夹中,然后我们还需要找到上面的Crash信息,然后右键导出该Crash信息,同样拷贝到CrashFolder文件夹中,接下来我们就可以利用atos来将Crash文件中的地址还原为代码。
atos的基本用法为:
$ atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>
在我本地中,我执行的是:atos -arch arm64 -o testImageSourceCode.app.dSYM/Contents/Resources/DWARF/testImageSourceCode -l 0x100d50000 0x100d55ee4
在上述命令中,需要解释的可能就是-l后面的两个参数,第一个参数是程序的基地址,也就是在crash文件中,在 Binary Images:
下面的第一行中的第一个以0X开头的地址,然后第二个参数就是Crash文件中错误信息的地址,执行完上述命令之后输出
-[LMTool test] (in testImageSourceCode) (LMTool.m:15)
字符串,该字符串就是对应代码中的方法以及对应的文件里面的行数。
另外一个方法就是使用一个第三方工具dSYMTools这里的用法和我们上述的差不多,只不过那里是可视化工具。
完整的demo已经上传到Github。