全面的理解和分析iOS的崩溃日志

yyuuzhu 2017-10-26 11:20:40 21872
本文来自 小小外星人 ,作者 yyuuzhu

写在前面:本文会在最开头将苹果官方的文档Understanding and Analyzing Application Crash Reports进行翻译,但这不仅仅是一篇翻译的文章,本文会让大家更加全面的了解ios的崩溃报告的获取、分析、用途。翻译的时候我会结合自己以往的使用经验来进行翻译。

理解和分析应用程序崩溃报告

重要提示:本文档包含有关开发中的一个接口或技术的初步信息。此信息将被更改,并根据本文档实现的软件应该用最终的操作系统软件进行测试。

当应用程序崩溃时,创建了一个崩溃报告,这是非常有用的了解什么造成的崩溃。本文件包含重要的信息,如何symbolicate,理解和解释的崩溃报告。

简介

当应用程序崩溃时,将创建一个崩溃报告并存储在设备上。崩溃报告描述的情况下,应用程序终止,在大多数情况下,包括每个执行线程的一个完整的回溯,通常对于调试应用中的问题是非常有用。你应该看看这些崩溃报告,了解你的应用程序有什么崩溃,然后尝试修复它们。

有回溯的崩溃报告需要被符号化了才可以分析。符号化(symbolication)成人们可读的函数名称和行号来取代内存地址。如果你通过Xcode的设备窗口来获取设备的崩溃日志,它们将在几秒钟后自动被符号化。否则,你将需要自己将崩溃日志符号化,通过自己导入崩溃文件到Xcode设备窗口。看到符号化(symbolicating)后的崩溃报告。

低内存报告不同于其他的崩溃报告,没有回朔在这种类型的报告。当低内存崩溃发生时,你必须调查你的内存使用模式和对低内存警告的响应。此文档指向了您可能会发现有用的多个内存管理引用。

获取崩溃和低内存报告

调试部署iOS应用程序 讨论如何直接从iOS设备获取崩溃和内存不足的报道。

应用程序分发指南中的分析崩溃报告 讨论如何从TestFlight beta测试者和那些已经从App Store下载应用程序的用户中收集崩溃报告。

symbolicating崩溃报告

symbolication是解决回溯地址的源代码的方法或函数名的过程,称为符号化。没有经过符号化的崩溃报告,是不能了解具体的崩溃信息的。

注:低内存报告不需要symbolicated。

注:来自MacOS崩溃报告通常是已经符号化的,或部分符号化后的(symbolicated)。

本节重点symbolicating从iOS,WatchOS,和TVOS的崩溃报告,但整体过程类似MacOS。

图1概述了崩溃报告和symbolication过程。

1.png

获取崩溃报告以及符号化过程

1.当编译器将源代码翻译成机器代码时,它也会生成调试符号,它将编译后的二进制中的每一个机器指令映射到源代码的行源代码中。根据调试信息格式(debug_information_format)编译设置,这些调试符号存储在二进制或在同伴的调试符号文件(dsym)。默认情况下,调试版本的应用程序的调试符号存储在编译后的二进制中,而发布版本的应用程序的调试符号存储在相应的dsym文中件以减少二进制大小。

调试符号文件和应用程序二进制文件与每一个build生成的UUID捆绑在一起。一个新的UUID生成是由build一个应用产生的,它应用程序每次build(编译打包)的唯一标识。即使一个功能相同的可执行文件是从相同的源代码重构,具有相同的编译器设置,也会有不同的生成的UUID。调试符号文件的后续版本,甚至来自同一个源文件,不会与其他版本的二进制文件相混淆。

2.当你的归档(archive)要分发的应用程序,Xcode会收集应用程序二进制随着.dsym文件,并且存放在home文件夹里。你可以在Xcode的组织者在“归档”部分,找到所有你归档后(archived)的应用。有关创建存档的更多信息,可以参考应用程序分发指南

重要:为了符号化(symbolicate)从测试人员,审查程序,和客户得到的崩溃报告,你必须保留每个存档您的应用程序。

3.如果你是通过App Store发布的应用程序,或使test flight进行测试,你将选择包括dsym文件到iTunes Connect。在提交对话框中,选择“您的应用程序的应用程序符号……”。为了接收来test flight收集的 以及 那些选择了分享的诊断数据的客户的崩溃报告,上传你的dsym文件是必要的 。有关崩溃报告服务的更多信息,可以参考应用程序分发指南。

重要:从app review收到的崩溃报告将是unsymbolicated,即使你有上传dsym文件到iTunes Connect。

你需要使用Xcode 来symbolicate任何从app review得到的崩溃报告。

看下午中"到symbolicating崩溃报告Xcode"。

4.当你的应用程序崩溃时,一个unsymbolicated崩溃报告会被创建并存储在设备上。

5.用户可以按照调试部署iOS应用程序的步骤,直接从他们的设备中检索崩溃报告。如果你的应用程序通过AdHoc or Enterprise distribution发布的,这是从你的用户获取崩溃报告的唯一途径。

6.从设备检索得到的未符号化的(unsymbolicated)崩溃报告需要使用Xcode来符号化(symbolicated)。Xcode使用您的应用程序的二进制在源代码中的源位置的回溯替换每个地址相关联的dsym文件。其结果是一个符号化的(symbolicated)崩溃报告。

7.如果用户选择了与苹果共享诊断数据,或者如果用户通过TestFlight安装了你的应用程序,崩溃报告会被上传到应用商店。

8.App Store 符号化崩溃报告 并且将类似的崩溃报告分组。类似的崩溃报告的集合被称为崩溃点。

9.符号化的崩溃报告是在Xcode's Crashes organizer提供给你的。

Bitcode

Bitcode是一个编译程序的中间表示。当你可以用bitcode来 archive an application,编译器产生的二进制文件包含bitcode而不是机器代码。一旦二进制已经上传到App Store,这可以被编译成机器码。App Store在将来,在没有任何行动的一部分的情况下,利用未来的改进的编译器,再次编译。

1.png

Bitcode compilation process

因为你的二进制最后的编译出现在App Store,Mac将不包含符号化从应用程序审查或用户发给你他们从设备中取得的崩溃报告所需要的调试符号文件(.dsym)。虽然dsym文件是您归档应用程序(archive your application)的时候产生的,它是为bitcode二进制并不能用来symbolicate崩溃报告。App Store在你从Xcode或从iTunes Connect网站,可以获得编译的bitcode并且可以下载的过程中,产生dsym文件。符号化从应用程序审查或用户发给你他们从设备中取得的崩溃报告,你必须下载这些dsym文件。崩溃报告获得通过崩溃报告服务将自动symbolicated。

重要:二进制应用程序商店会比最初二进制编译,有不同的UUID

从Xcode获取dSYMs文件

1.下载dsyms文件

在归档管理中选择相应的归档并下载dsyms文件

1.png

1.png

2.在归档出的文件中找到dSYMs文件

1.png

1.png

1.png

确定崩溃报告是否被符号化

1.png

符号化崩溃报告

1.符号化用xcode编译安装软件的设备上的崩溃报告

当应用在设备上运行,遇到崩溃的时候会产生崩溃日志。如果这个应用是用xcode直接在设备上编译运行的,那么就可以将手机连接到编译的时候所用的电脑上,打开xcode,在Window的Devices中去查看日志。找到日志的时候,xcode会自己去符号化崩溃日志。

1.png

选取设备

1.png

查看设备日志

1.png

xcode自己去符号化崩溃文件

2.符号化用安装包安装在测试设备上的应用所产生的崩溃日志

符号化的时候需要准备symbolicatecrash文件 .dSYM文件 以及.app文件

符号化前先检查一下三者的uuid是不是一致的,只有是一致的才能符号化成功。

查看xx.app文件的uuid的方法:

dwarfdump --uuid xxx.app/xxx (xxx工程名)

查看xx.app.dSYM文件的uuid的方法令:

dwarfdump --uuid xxx.app.dSYM (xxx工程名)

而.crash的uuid位于,crash日志中的Binary Images:中的第一行尖括号内。如:armv7<8bdeaf1a0b233ac199728c2a0ebb4165>

将三种文件拷贝到同一个目录中,在终端中使用命令

./symbolicatecrash ./.crash ./.app.dSYM > xxx.crash来解析崩溃日志。

1.png

如果想今后解析日志的时候更方便一点,特别是解析多个崩溃日志的时候,如果一个一个去解析的话,很花费时间的。我的话,会将这些写到一个脚本中,解析的时候就只用执行脚本,就可以很方便快捷的获取到崩溃日志。

3.获取并符号化线上的崩溃报告

一.通过打包上线时的xcode来获取线上的崩溃报告

线上app的崩溃日志会被app store收集并符号化分组。类似的崩溃报告的集合被称为崩溃点。(如果用户选择了与苹果共享诊断数据,这些崩溃日志才会被收集并被符号化)

1.png

在点击相应应用后,会显示此应用的崩溃集合。可以看到每一个集合中都会有很多个设备,如果右键进去查看的话,会看到很多文件。

1.png

1.png

再次右键进去查看的话,会最终看到详细的崩溃日志

1.png

当选中了一个崩溃集合后,如果选择在项目中打开,会在项目代码中找到具体出问题的代码

1.png

1.png

1.png

二、通过友盟等第三方工具来获取崩溃日志

崩溃日志列表

1.png

其中的一个崩溃日志

1.png

崩溃的代码的位置,这个是最关键的,可以通过这个来找到代码中的出问题的地方

1.png

export dSYMPath="$(find ~/Library/Developer/Xcode -iname '*.dSYM' -print0 | xargs -0 dwarfdump -u | grep C0349572-9622-3A00-81D0-4DDE0E00DC7A | sed -E 's/^{FNXX==XXFN}+//' | head -n 1)";是为了找到归档时候产生的dsym文件的路径

dwarfdump --arch=armv7 --lookup 0x426031 "$dSYMPath"是符号化的关键,可以找出出问题的地方。

1.png

分析符号化后的崩溃报告

1.头部

每个崩溃报告都会以一个头部开始

Listing 4  Excerpt of the header from a crash report.
Incident Identifier: B6FD1E8E-B39F-430B-ADDE-FC3A45ED368C
CrashReporter Key: f04e68ec62d3c66057628c9ba9839e30d55937dc
Hardware Model: iPad6,8
Process: TheElements [303]
Path: /private/var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
Identifier: com.example.apple-samplecode.TheElements
Version: 1.12
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: com.example.apple-samplecode.TheElements [402]
Date/Time: 2016-08-22 10:43:07.5806 -0700
Launch Time: 2016-08-22 10:43:01.0293 -0700
OS Version: iPhone OS 10.0 (14A5345a)
Report Version: 104

以下的值是特别值得注意的:

Incident Identifier: 一个唯一的标识. 不会存在共用一个标识的崩溃报告.

CrashReporter Key:是与设备标识相对应的唯一键值。虽然它不是真正的设备标识符,但也是一个非常有用的情报:如果你看到100个崩溃日志的CrashReporter Key值都是相同的,或者只有少数几个不同的CrashReport值,说明这不是一个普遍的问题,只发生在一个或少数几个设备上。

Process: 是应用名称。中括号里面的数字是闪退时应用的进程ID。

Version: 崩溃进程的版本号. 这个值包含在 CFBundleVersion and CFBundleVersionString中.

Code Type: 崩溃日志所在设备的架构. 会是ARM-64, ARM, x86-64, or x86中的一个.

OS Version: 崩溃发生时的系统版本

异常信息

异常信息会列出异常的类型、位置。

以下的内容是摘录的一个崩溃报告的异常代码段,该崩溃报告是一个进程由于一个未捕获的异常而崩溃产生的。

Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0

以下的内容是摘录于一个因为空指针的访问而崩溃产生的崩溃报告的异常代码段

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [0]
Triggered by Thread: 0

异常信息中可能出现的字段的解释如下:

Exception Codes: 处理器的具体信息有关的异常编码成一个或多个64位进制数。通常情况下,这个区域不会被呈现,因为将异常代码解析成人们可以看懂的描述是在其它区域进行的。

Exception Subtype: 供人们可读的异常代码的名字

Exception Message: 从异常代码中提取的额外的可供人们阅读的信息.

Exception Note: 不是特定于一个异常类型的额外信息.如果这个区域包含SIMULATED (这不是一个崩溃)然后进程没有崩溃,但是被watchdog杀掉了

Termination Reason: 当一个进程被终止的时的原因。

Triggered by Thread: 异常所在的线程.

下面的小节将介绍一些最常见的异常类型。

Bad Memory Access [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]

进程试图访问无效的内存,或试图以内存的保护级别所不允许的方式去访问内存(例如,写入到只读存储器)。异常类型字段(Exception Subtype)包含一个kern_return_t描述错误,和错误的访问的内存地址。

这里是调试一个Bad Memory Access的一些小技巧:

1.如果objc_msgSend或者objc_release在回溯(Backtraces)的顶部附近,这个进程可能是尝试给一个释放的对象发送消息。你应该用Zombies instrument(调试僵尸对象的工具)来更好的理解这个崩溃。

2.如果gpus_ReturnNotPermittedKillClient在回溯的顶部附近,这个进程是由于在后台尝试用OpenGL ES 或者 Metal来渲染,而被杀掉的。See QA1766: How to fix OpenGL ES application crashes when moving to the background.

3.用地址消毒剂(Address Sanitizer,这是xcode7引入的新特性 )来跑程序。

The address sanitizer adds additional instrumentation around memory access in your compiled code. As your application runs, Xcode will alert you if memory is accessed in a way that could lead to a crash.

Abnormal Exit (异常退出)[EXC_CRASH // SIGABRT]

进程异常退出。该异常类型崩溃最常见的原因是未捕获的Objective-C和C++异常和调用abort()。

如果他们需要太多的时间来初始化,程序将被终止,因为触发了看门狗。如果是因为启动的时候被挂起,所产生的崩溃报告异常类型(Exception Subtype)将是launch_hang。

Trace Trap (追踪捕获)[EXC_BREAKPOINT // SIGTRAP]

类似于异常退出,这个异常是为了给附加的调试器中断的过程的机会在其执行一个特定的点。您可以从您自己的代码引发此异常使用__builtin_trap()函数。如果没有调试器连接,进程将被终止并生成崩溃报告。

Illegal Instruction(非法指令) [EXC_BAD_INSTRUCTION // SIGILL]

进程试图执行非法或未定义指令。这个进程可能试图通过一个配置错误的函数指针,跳到一个无效的地址。在英特尔处理器,ud2操作码造成EXC_BAD_INSTRUCTION异常但通常用于调试的时候的追踪。在英特尔处理器上的swift代码因为这个异常类型而被终止,如果在运行时遇到意外情况。有关详细信息,请参阅追踪捕获。

Resource Limit [EXC_RESOURCE]

这个进程超出了资源消耗的限制。这是一个从操作系统通知,进程是使用太多的资源。这虽然不是崩溃但也会生成崩溃日志。

其它的异常信息

0x8badf00d: 读做 “ate bad food”! (把数字换成字母,是不是很像 :p)该编码表示应用是因为发生watchdog超时而被iOS终止的。 通常是应用花费太多时间而无法启动、终止或响应用系统事件。

0xbad22222: 该编码表示 VoIP 应用因为过于频繁重启而被终止。

0xdead10cc: 读做 “dead lock”!该代码表明应用因为在后台运行时占用系统资源,如通讯录数据库不释放而被终止 。

0xdeadfa11: 读做 “dead fall”! 该代码表示应用是被用户强制退出的。根据苹果文档, 强制退出发生在用户长按开关按钮直到出现 “滑动来关机”, 然后长按 Home按钮。强制退出将产生 包含0xdeadfa11 异常编码的崩溃日志, 因为大多数是强制退出是因为应用阻塞了界面。

待补充

其它捕捉和符号化崩溃日志的方法

1.通过代码来捕捉崩溃信息,并发送到指定的地方

在项目的代码中重新指定顶层的错误处理的handler,这样可以在软件崩溃前的最后一秒调用错误处理的函数来获得崩溃的信息(这里的信息已经是被符号化的)。然后将崩溃信息发送到自己的服务器或者其它的地方,以便开发者巩固自己的代码的可靠性。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[AFNetworkReachabilityManager sharedManager] startMonitoring];
    appDelegate = self;
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
    _notification = notification;
    NSSetUncaughtExceptionHandler(&caughtExceptionHandler);
    /*Changes the top-level error handler.
    Sets the top-level error-handling function where you can perform last-minute logging before the program terminates
    */
    return YES;
}
void caughtExceptionHandler(NSException *exception){
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========\\nname:%@\\nreason:\\n%@\\ncallStackSymbols:\\n%@",name,reason,[callStack componentsJoinedByString:@"\\n"]];
    //把异常崩溃信息发送至开发者邮件
    NSMutableString *mailUrl = [NSMutableString string];
    [mailUrl appendString:@"mailto:xxx@qq.com"];
    [mailUrl appendString:@"?subject=程序异常崩溃信息,请配合发送异常报告,谢谢合作!"];
    [mailUrl appendFormat:@"&body=%@", content];
    // 打开地址
    NSString *mailPath = [mailUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:mailPath]];
}