主要内容
- 闪退捕获
- 日志分析
闪退捕获
- 内核级异常:Mach异常->Unit信号(Mach层捕获到异常通过发送信号到Unit层)
- 应用层异常:NSException
本文实现方式:捕获Unit信号和捕获NSException
为什么要分两类
在没有设置捕获NSException时,因为某个NSException导致异常时,Mach异常或Unit信号也是可以捕获到的只是有些不完成。
捕获Unit信号获取的异常
reason:Signal SIGABRT was raise
callStackSymbols:
0 MTCrashCaughtDemo 0x00000001000c321c signalExceptionHandler + 196
1 libsystem_platform.dylib 0x0000000183a2193c _sigtramp + 52
2 libsystem_pthread.dylib 0x0000000183a28ef8 pthread_kill + 112
3 libsystem_c.dylib 0x00000001838cddc8 abort + 140
4 libc++abi.dylib 0x00000001834013f4 __cxa_bad_cast + 0
5 libc++abi.dylib 0x000000018341de98 + 0
6 libobjc.A.dylib 0x0000000183428248 + 124
7 libc++abi.dylib 0x000000018341af44 + 16
8 libc++abi.dylib 0x000000018341ab10 __cxa_rethrow + 144
9 libobjc.A.dylib 0x0000000183428120 objc_exception_rethrow + 44
10 CoreFoundation 0x0000000183ca0cf8 CFRunLoopRunSpecific + 552
11 GraphicsServices 0x0000000185588088 GSEventRunModal + 180
12 UIKit 0x0000000188f86088 UIApplicationMain + 204
13 MTCrashCaughtDemo 0x00000001000c45ac main + 124
14 libdyld.dylib 0x000000018383e8b8 + 4
捕获NSException的异常
name:NSRangeException
reason:*** -[__NSArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]
0 CoreFoundation 0x0000000183dc2dc8 + 148
1 libobjc.A.dylib 0x0000000183427f80 objc_exception_throw + 56
2 CoreFoundation 0x0000000183ca3098 CFRunLoopRemoveTimer + 0
3 MTCrashCaughtDemo 0x000000010001c1e0 -[ViewController test3] + 192
4 MTCrashCaughtDemo 0x000000010001c088 -[ViewController touchUpInsideButton:] + 68
5 UIKit 0x0000000188f54be8 + 100
6 UIKit 0x0000000188f54b64 + 80
7 UIKit 0x0000000188f3c870 + 436
8 UIKit 0x0000000188f54454 + 572
9 UIKit 0x0000000188f54084 + 804
10 UIKit 0x0000000188f4cc20 + 784
11 UIKit 0x0000000188f1d04c + 248
12 UIKit 0x0000000188f1b628 + 6568
13 CoreFoundation 0x0000000183d7909c + 24
14 CoreFoundation 0x0000000183d78b30 + 540
15 CoreFoundation 0x0000000183d76830 + 724
16 CoreFoundation 0x0000000183ca0c50 CFRunLoopRunSpecific + 384
17 GraphicsServices 0x0000000185588088 GSEventRunModal + 180
18 UIKit 0x0000000188f86088 UIApplicationMain + 204
19 MTCrashCaughtDemo 0x000000010001c564 main + 124
20 libdyld.dylib 0x000000018383e8b8 + 4
PS:当同时设置捕获NSException和捕获Unix信号时,如果某个NSException出现异常,Unix信号那边是得不到回调的,所以不要担心同一个异常出现两份日志
捕获Unix信号实现
例如:捕获SIGABRT信号
注册监听信号
signal(SIGABRT, signalExceptionHandler)
注销监听信号
signal(SIGABRT, SIG_DFL)
读取异常数据
void signalExceptionHandler(int signal)
{
NSMutableString *exceptionDescription = [NSMutableString string];
[exceptionDescription appendFormat:@"reason:Signal %d was raise\n", signal];
[exceptionDescription appendString:@"callStackSymbols:\n"];
void *callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
for(int i = 0; i < frames; ++i)
{
[exceptionDescription appendFormat:@"%s\n", strs[i]];
}
}
Unit信号定义在
中,至于每种信号对应的描述可以参考这里iOS异常捕获
捕获NSException实现
注册监听
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler)
读取异常数据
void uncaughtExceptionHandler(NSException *exception)
{
NSMutableString *exceptionDescription = [NSMutableString string];
[exceptionDescription appendFormat:@"name:%@\n", exception.name];
[exceptionDescription appendFormat:@"reason:%@\n",exception.reason];
for(NSString *symbol in exception.callStackSymbols)
{
[exceptionDescription appendFormat:@"%@\n", symbol];
}
}
PS:Unix信号在Xcode是Run是读取不到的,可以先在Xcode上Run然后Stop,再到模拟器上打开Build的App
日志分析
日志的来源可以分两类
- 苹果收集
- 开发者收集
苹果收集日志(只提取了关键信息)
0 CoreFoundation 0x183dc2db0 __exceptionPreprocess + 124
1 libobjc.A.dylib 0x183427f80 objc_exception_throw + 56
2 CoreFoundation 0x183dc2c80 +[NSException raise:format:arguments:] + 108
3 Foundation 0x184748154 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 112
4 UIKit 0x189115808 -[UITableView _endCellAnimationsWithContext:] + 12624
5 UIKit 0x1891301d0 -[UITableView _updateRowsAtIndexPaths:updateAction:withRowAnimation:] + 360
6 InterviewEvaluation 0x10006b3a0 0x100040000 + 177056
7 UIKit 0x189053dc4 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1316
8 UIKit 0x1891117d4 -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 376
9 UIKit 0x1891cf0c8 _runAfterCACommitDeferredBlocks + 292
10 UIKit 0x1891dca80 _cleanUpAfterCAFlushAndRunDeferredBlocks + 92
11 UIKit 0x188f0e5a4 _afterCACommitHandler + 96
12 CoreFoundation 0x183d78728 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
13 CoreFoundation 0x183d764cc __CFRunLoopDoObservers + 372
14 CoreFoundation 0x183d768fc __CFRunLoopRun + 928
15 CoreFoundation 0x183ca0c50 CFRunLoopRunSpecific + 384
16 GraphicsServices 0x185588088 GSEventRunModal + 180
17 UIKit 0x188f86088 UIApplicationMain + 204
18 InterviewEvaluation 0x1000591f8 0x100040000 + 102904
19 libdyld.dylib 0x18383e8b8 start + 4
开发者收集日志(使用上面介绍的方式)
name:NSInternalInconsistencyException
reason:Invalid update: invalid number of rows in section 0. The number of rows contained in an
existing section after the update (2) must be equal to the number of rows contained in that
section before the update (1), plus or minus the number of rows inserted or deleted from that
section (1 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that
section (0 moved in, 0 moved out).
0 CoreFoundation 0x0000000183dc2dc8 + 148
1 libobjc.A.dylib 0x0000000183427f80 objc_exception_throw + 56
2 CoreFoundation 0x0000000183dc2c80 + 0
3 Foundation 0x0000000184748154 + 112
4 UIKit 0x0000000189115808 + 12624
5 UIKit 0x00000001891301d0 + 360
6 InterviewEvaluation 0x00000001001273a0 InterviewEvaluation + 177056
7 UIKit 0x0000000189053dc4 + 1316
8 UIKit 0x00000001891117d4 + 376
9 UIKit 0x00000001891cf0c8 + 292
10 UIKit 0x00000001891dca80 + 92
11 UIKit 0x0000000188f0e5a4 + 96
12 CoreFoundation 0x0000000183d78728 + 32
13 CoreFoundation 0x0000000183d764cc + 372
14 CoreFoundation 0x0000000183d768fc + 928
15 CoreFoundation 0x0000000183ca0c50 CFRunLoopRunSpecific + 384
16 GraphicsServices 0x0000000185588088 GSEventRunModal + 180
17 UIKit 0x0000000188f86088 UIApplicationMain + 204
18 InterviewEvaluation 0x00000001001151f8 InterviewEvaluation + 102904
19 libdyld.dylib 0x000000018383e8b8 + 4
UUID: 5FFEDD96-E4E1-3008-8D92-D22C2054D9C8
loadAddress: 0x1000fc000
PS:是不是有种懵逼的感觉苹果收集的日志一到关键的地方就看不懂,至于开发者收集的就更看不懂了
关于dSYM文件
- dSYM文件就是一个内存地址和函数的映射表
- 每次Archive(打包)都会产生一个dSYM文件与ipa包对应
也就是说只要有dSYM文件就可以解析上面的内存地址了,那么dSYM怎么获取呢?
- 如果是脚本打包,完成之后,ipa包的旁边一般会有个dSYM文件
- 如果是Xcode打包,完成之后,在Xcode中工具栏上 Window->Organizeer->选择相应的App->Archives->选择相应的包 进入选中的包然后找dSYMs目录
PS:因为每次Archive生成的dSYM文件都是不一样的,当我们发布了很多版本后,获取到的日志匹配dSYM文件就麻烦了,所以我们需要使用一种方式把闪退日志和dSYM文件相互对应
关于UUID
- 每个dSYM文件都有一个UUID
- 每个ipa包也一个UUID
也就是说只要在产生日志时获取当前ipa包的UUID,并和日志一起上传就可以了
dSYM的UUID获取方式
命令:dwarfdump --uuid appName.app.dSYM
例如:dwarfdump --uuid /Users/mtry/Desktop/155401/InterviewEvaluation.app.dSYM
结果:UUID: E1F36C5C-5F16-3FC4-8F51-BE25B49C0B1D (armv7)
UUID: 5FFEDD96-E4E1-3008-8D92-D22C2054D9C8 (arm64)
开发者自己收集日志,出现闪退时获取当前ipa包的UUID,来自于stackoverflow
#import
NSString *executableUUID()
{
const uint8_t *command = (const uint8_t *)(&_mh_execute_header + 1);
for (uint32_t idx = 0; idx < _mh_execute_header.ncmds; ++idx) {
if (((const struct load_command *)command)->cmd == LC_UUID) {
command += sizeof(struct load_command);
return [NSString stringWithFormat:@"%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
command[0], command[1], command[2], command[3],
command[4], command[5],
command[6], command[7],
command[8], command[9],
command[10], command[11], command[12], command[13], command[14], command[15]];
} else {
command += ((const struct load_command *)command)->cmdsize;
}
}
return nil;
}
苹果收集的日志,自动带上了UUID,可以在日志的里面找到比如
PS:图片标记了两个关键信息,一个UUID,另一个时加载地址后面会提到
苹果收集的日志解析
获取当前设备的闪退日志:连上你的Mac,在Xcode中工具栏 Window->Devices->DEVICES 中选择你的设备->View Device Logs
比如上面的闪退在匹配上dSYM文件之后解析的结果
获取苹果收集用户的闪退日志:在Xcode中工具栏 Window->Organizeer->选择你的App->Crashes->选择版本,就可以下载了
解析方式:
- 下载完的日志可以直接拖进上面一步的列表中进行解析
- 使用命令行工具symbolicatecrash,如果解析的日志很多建议使用这种,因为可以写脚本。具体使用看这里Symbolicating Your iOS Crash Reports
注意:
- 只有按照苹果要求的格式收集的日志才可以使用上面的方式解析,至于格式是什么样的看下苹果的日志就知道了
- 苹果收集普通用户的闪退日志需要用户在:设置->隐私->诊断于用量->自动发送->与应用开发者共享 是开启的,默认是关闭的,所以一些上线的App可以收集的闪退日志还是相当有限的。对于一些有需要TestFlight的App是非常好的,
因为TestFlight的用户默认自动发送日志
,具体可以看这里Analyzing Crash Reports
开发者收集的日志解析
使用苹果的atos命令
atos [-o AppName.app/AppName] [-l loadAddress] [-arch architecture] [address ...]
方式一:需要AppName.app和dSYM文件(AppName.ipa解压出AppName.app)
例如:atos -o /Users/mtry/Desktop/155401/InterviewEvaluation.app/InterviewEvaluation -l 0x1000fc000 -arch arm64 0x00000001001273a0
结果:-[IECandidateFilterPopView tableView:didSelectRowAtIndexPath:] (in InterviewEvaluation) (IECandidateFilterPopView.m:382)
方式二:只需要dSYM文件
例如:atos -o /Users/mtry/Desktop/155401/InterviewEvaluation.app.dSYM/Contents/Resources/DWARF/InterviewEvaluation -l 0x1000fc000 -arch arm64 0x00000001001273a0
结果:-[IECandidateFilterPopView tableView:didSelectRowAtIndexPath:] (in InterviewEvaluation) (IECandidateFilterPopView.m:382)
loadAddress地址的获取方式
方式一:内存地址 - 偏移量 = 加载地址(比如上面闪退日志)
0x00000001001273a0 - 0x2b3a0(177056) = 0x00000001001151f8 - 0x191f8(102904) = 0x1000fc000
方式二:代码实现来源于iOS Crash Report 的加载地址、dSYM 与 UUID
#include
uintptr_t get_load_address(void) {
const struct mach_header *exe_header = NULL;
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
const struct mach_header *header = _dyld_get_image_header(i);
if (header->filetype == MH_EXECUTE) {
exe_header = header;
break;
}
}
return (uintptr_t)exe_header;
}
GitHub具体实现
MTCrashCaught
参考资料
漫谈iOS Crash收集框架
iOS异常捕获
Analyzing Crash Reports
Understanding and Analyzing iOS Application Crash Reports
Symbolicating Your iOS Crash Reports
分析iOS Crash文件:符号化iOS Crash文件的3种方法
iOS Crash Report 的加载地址、dSYM 与 UUID