这篇文章其实只讲述一个问题, 那就是我们捕获到crash日志之后 再到 送去解析, 在这中间到底还要做什么处理.
目前市面上流通的crash分析SDK有很多, 比如腾讯bugly, 网易云捕, 等等, 用起来也很简单: 引入到工程, 初始化, 设好回调, 就OK了. 然后当我们在后台看到上报的日志后, 只要上传一下符号表, 就可以轻松的解析出问题发生的代码了
随便贴个例子:
Application Specific Information:
*** Terminating app due to uncaught exception 'RLMException', reason: 'Object has been deleted or invalidated.'
Last Exception Backtrace:
0 CoreFoundation __exceptionPreprocess +228
1 libobjc.A.dylib objc_exception_throw +56
2 MyProject ____ZN12_GLOBAL__N_118makeOptionalGetterIxEEP11objc_objectm_block_invoke /Users/dingjielu/Documents/Netease/avg-client-ios/Pods/Realm/include/RLMObject_Private.hpp:0 +0
3 MyProject -[HomeViewController tableView:cellForRowAtIndexPath:] /路径/马赛克.m:300 +16
4 UIKitCore -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] +684
5 UIKitCore -[UITableView _createPreparedCellForGlobalRow:willDisplay:] +80
我们可以很轻松的定位出如下结论: 问题就是出在 HomeViewController tableView:cellForRowAtIndexPath, 300行附近.
用别人的SDK确实很爽很香,
但是, 当我们有一些额外的需求的时候, 就不得不去手动上报了. (比如商业原因不能用竞品SDK, 自家的又垃圾, 又比如需要更精准的定位某几个业务场景的crash日志)
于是开发同学就接到了自己手动捕获并且解析 崩溃日志 的需求了.
然后就去查资料, 发现大部分资料都这么说: 捕获分为NSException捕获, Singal捕获, 我们要做的就是搜集调用堆栈, 然后进行符号化
, 这句好似公式一样的话, 存在于各种文档, 资料内.
当我们按照前辈的经验(其实就是抄了一段捕获日志的代码), 兴致BoBo的接入捕获之后, 发现我们上报得到的日志都是这样的
crash_name: 信号量崩溃捕获
crash_reason: signal 6 was raised
0 MyProject 0x0000000104f4b1f4 MyProject + 2535924
1 MyProject 0x0000000104f4a1ac MyProject + 2531756
2 libsystem_platform.dylib 0x0000000182308b48 _sigtramp + 36
3 libsystem_pthread.dylib 0x000000018230e288 <redacted> + 376
4 libsystem_c.dylib 0x00000001820dbd0c abort + 140
5 libc++abi.dylib 0x00000001818772c8 __cxa_bad_cast + 0
6 libc++abi.dylib 0x0000000181877470 <redacted> + 0
7 libobjc.A.dylib 0x00000001818a08d4 <redacted> + 124
8 libc++abi.dylib 0x000000018189137c <redacted> + 16
9 libc++abi.dylib 0x0000000181890f78 __cxa_rethrow + 144
10 libobjc.A.dylib 0x00000001818a07ac objc_exception_rethrow + 44
11 CoreFoundation 0x00000001825ace18 CFRunLoopRunSpecific + 664
12 GraphicsServices 0x0000000184592020 GSEventRunModal + 100
13 UIKit 0x000000018c5cc758 UIApplicationMain + 236
14 MyProject 0x0000000104d2618c MyProject + 287116
15 libdyld.dylib 0x000000018203dfc0 <redacted> + 4
这个时候, 你去解析, 就会发现, 根本解析不了, 为什么?
比如说我们用atos去解析, 解析出来会发现, 什么有用的信息都没产生
我们来看下atos的调用方式:
atos -o [dsym file path] -l [Load Address] -arch [arch type] [Stack Address]
参数解释:
dsym file path
: 很好理解, 符号表地址
arch type
: 架构类型 (armv7, arm64等)
Stack Address
: 崩溃栈
Load Address
: 崩溃对象的实际加载地址
我们再来看别人文章内贴出的教学崩溃日志例子. 一般是长这个样子:
Exception Type: SIGTRAP
Exception Codes: #0 at 0x1fd758624
Crashed Thread: 0
Thread 0 Crashed:
0 CoreFoundation CFRelease +124
1 MyProject 0x00000001028ad018 0x1025a4000 + 3182616
2 MyProject 0x00000001028accf4 0x1025a4000 + 3181812
3 MyProject 0x00000001028ac104 0x1025a4000 + 3178756
4 MyProject 0x00000001028acb68 0x1025a4000 + 3181416
5 MyProject 0x0000000102930d84 0x1025a4000 + 3722628
6 MyProject 0x00000001029306e0 0x1025a4000 + 3720928
7 MyProject 0x0000000102938c88 0x1025a4000 + 3755144
8 libdispatch.dylib _dispatch_call_block_and_release +24
9 libdispatch.dylib _dispatch_client_callout +16
10 libdispatch.dylib _dispatch_main_queue_callback_4CF$VARIANT$mp +1068
我们随便拿出一行:
1 MyProject 0x00000001028ad018 0x1025a4000 + 3182616
这一行的参数是这样的
MyProject
: 回溯对象
0x00000001028ad018
: 崩溃栈
0x1025a4000
: 崩溃对象的实际加载地址
+3182616
函数偏移值
而我们上面获取到的崩溃信息是这样的:
MyProject 0x0000000104f4b1f4 MyProject + 2535924
很明显, 第三列参数, 也就是崩溃对象的实际加载地址看不到, 只看到了这个对象的名字!
所以, 问题的本质就是, 我们捕获崩溃时候光去获取backtrace是不够的, 还需要获取崩溃对象的实际物理加载地址
而实质上, 这个参数需要我们去获取崩溃发生时候的 BinaryImage信息, 然后去找到对应的地址替换进去才可以
获取Binary Image中的关键信息后. 大概会得到这样的东西:
Binary Images:
0x1025a4000 - 0x103f2ffff +MyProject arm64 <0171fc9962a932eeb17c78e8b2ab5191> /var/containers/Bundle/Application/D37F848C-3DB9-44D4-AC89-0069E1AD6CAA/MyProject.app/MyProject
0x10464c000 - 0x104657fff libobjc-trampolines.dylib arm64 <065bd8006d513c358dc14e2a8ff1ba31> /usr/lib/libobjc-trampolines.dylib
0x1fc9d7000 - 0x1fc9d8fff libSystem.B.dylib arm64 <8a05d5f48f0a376abe6bd1caf4fc8138> /usr/lib/libSystem.B.dylib
0x1fc9d9000 - 0x1fca2efff libc++.1.dylib arm64 <6a272068f00d37a984e331ba58e1c3c4> /usr/lib/libc++.1.dylib
0x1fca2f000 - 0x1fca41fff libc++abi.dylib arm64 <d4920e50788d3be6bccf8f550bc6d491> /usr/lib/libc++abi.dylib
0x1fca42000 - 0x1fd1c9fff libobjc.A.dylib arm64 <1167a03d9f853f34a96fd96818ad77b5> /usr/lib/libobjc.A.dylib
0x1fd1ca000 - 0x1fd1cefff libcache.dylib arm64 <bd876f73c3ff39459113c6fa56d15e3d> /usr/lib/system/libcache.dylib
0x1fd1cf000 - 0x1fd1dafff libcommonCrypto.dylib arm64 <b6d7765a17a93ff4a095ee57139d16c5> /usr/lib/system/libcommonCrypto.dylib
0x1fd1db000 - 0x1fd1dffff libcompiler_rt.dylib arm64 <644550d26c693e95affb4ce0b8c5c7a6> /usr/lib/system/libcompiler_rt.dylib
可以看到, 第一栏就是我们需要的 MyProject 实际的地址.
我们可以通过如下方法获取 Binary Image的必要信息:
+ (NSArray *)loadBinayImages {
NSMutableArray *arr = [[NSMutableArray alloc] init];
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
uint64_t vmbase = 0;
uint64_t vmslide = 0;
uint64_t vmsize = 0;
uint64_t loadAddress = 0;
uint64_t loadEndAddress = 0;
NSString *imageName = @"";
NSString *uuid;
const struct mach_header *header = _dyld_get_image_header(i);
const char *name = _dyld_get_image_name(i);
vmslide = (i);
imageName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
BOOL is64bit = header->magic == MH_MAGIC_64 || header->magic == MH_CIGAM_64;
uintptr_t cursor = (uintptr_t)header + (is64bit ? sizeof(struct mach_header_64) : sizeof(struct mach_header));
struct load_command *loadCommand = NULL;
for (uint32_t i = 0; i < header->ncmds; i++, cursor += loadCommand->cmdsize) {
loadCommand = (struct load_command *)cursor;
if(loadCommand->cmd == LC_SEGMENT) {
const struct segment_command* segmentCommand = (struct segment_command*)loadCommand;
if (strcmp(segmentCommand->segname, SEG_TEXT) == 0) {
vmsize = segmentCommand->vmsize;
vmbase = segmentCommand->vmaddr;
}
} else if(loadCommand->cmd == LC_SEGMENT_64) {
const struct segment_command_64* segmentCommand = (struct segment_command_64*)loadCommand;
if (strcmp(segmentCommand->segname, SEG_TEXT) == 0) {
vmsize = segmentCommand->vmsize;
vmbase = (uintptr_t)(segmentCommand->vmaddr);
}
}
else if (loadCommand->cmd == LC_UUID) {
const struct uuid_command *uuidCommand = (const struct uuid_command *)loadCommand;
NSString *uuidString = [[[NSUUID alloc] initWithUUIDBytes:uuidCommand->uuid] UUIDString];
uuid = [[uuidString stringByReplacingOccurrencesOfString:@"-" withString:@""] lowercaseString];
}
}
loadAddress = vmbase + vmslide;
loadEndAddress = loadAddress + vmsize - 1;
NSString *str = [NSString stringWithFormat:@"%llx - %llx %@ + %@",loadAddress, loadEndAddress, imageName, uuid];
[arr addObject:str];
}
return arr;
}
到这里. 我们就拿到loadAddress 和 loadEndAddress了, 至于具体怎么解析, 之后再出文章解释吧.
参考文章:
iOS实现Crash捕获与堆栈符号化, ps:这篇文章写的确实比较详细
我是火球猫, 谢谢观看我的文章, 有问题的话就留言吧.