异常捕获和分析

主要内容

  • 闪退捕获
  • 日志分析

闪退捕获

  • 内核级异常: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怎么获取呢?

  1. 如果是脚本打包,完成之后,ipa包的旁边一般会有个dSYM文件
  2. 如果是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,可以在日志的里面找到比如

image_1.png

PS:图片标记了两个关键信息,一个UUID,另一个时加载地址后面会提到

苹果收集的日志解析

获取当前设备的闪退日志:连上你的Mac,在Xcode中工具栏 Window->Devices->DEVICES 中选择你的设备->View Device Logs

比如上面的闪退在匹配上dSYM文件之后解析的结果

异常捕获和分析_第1张图片
image_2.png

获取苹果收集用户的闪退日志:在Xcode中工具栏 Window->Organizeer->选择你的App->Crashes->选择版本,就可以下载了

解析方式:

  1. 下载完的日志可以直接拖进上面一步的列表中进行解析
  2. 使用命令行工具symbolicatecrash,如果解析的日志很多建议使用这种,因为可以写脚本。具体使用看这里Symbolicating Your iOS Crash Reports

注意:

  1. 只有按照苹果要求的格式收集的日志才可以使用上面的方式解析,至于格式是什么样的看下苹果的日志就知道了
  2. 苹果收集普通用户的闪退日志需要用户在:设置->隐私->诊断于用量->自动发送->与应用开发者共享 是开启的,默认是关闭的,所以一些上线的App可以收集的闪退日志还是相当有限的。对于一些有需要TestFlight的App是非常好的,因为TestFlight的用户默认自动发送日志,具体可以看这里Analyzing Crash Reports
异常捕获和分析_第2张图片
image_3.png

开发者收集的日志解析

使用苹果的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

你可能感兴趣的:(异常捕获和分析)