理解和分析App崩溃报告(译)

Apple Develop官方原文:Understanding and Analyzing Application Crash Reports

简介

当一个应用程序崩溃, 会产生一个崩溃报告(crash report) ,并存储到那个设备。崩溃报告描述了应用程序崩溃的条件,大多数情况会包含每个执行线程的一个完整的回溯,对我们调试应用程序的问题是非常有用的。你应该查看崩溃报告来了解你应用程序有什么崩溃,然后尝试修复它们。
崩溃报告必须被符号化(symbolicated),才能够被分析。符号化就是将内存地址替换成人能够阅读的函数名和代码所处的行数(即第几行)。如果你通过Xcode的Devices窗口,从一个设备中获取崩溃日志,几秒钟后会自动为你完成符号化。否则,你需要将.crash文件导入到Xcode的Devices窗口,来将它符号化。
低内存报告(Low Memory report)和崩溃报告不一样,这类型的报告没有回溯。当一个低内存报告发生时,你必须检查你的内存使用模式以及对低内存警告的应对方法。这篇文章将提供几种内存管理参考,你可能会有用。

获取崩溃和低内存报告

Debugging Deployed iOS Apps讨论怎样直接从设备获取崩溃和低内存报告。
App Distribution Guide中的Analyzing Crash Reports会讨论如何查看从TestFlight beta测试者以及从App Store下载你App的用户中收集的全部崩溃报告。

符号化崩溃报告

符号化是把回溯地址(即符号)还原成源码方法或者函数名的过程。不先把崩溃报告进行符号化,是很难判断崩溃发生在哪里。

Note:低内存报告不需要进行符号化。

Note:来至macOS的崩溃报告通常在它们产生的时候就已经符号化的,或者部分符号化的。这部分集中讨论来之iOS,watchOS和tvOS,但是总体过程和macOS相似。

Figure 1:崩溃的报告以及符号化过程的总览。

总览

  1. 在编译器将你的源代码翻译成机器码的同时,它也会生成debug symbols (调试符号),它在编译后的二进制中,映射每个机器指令到原来的每一行源代码。这些调试符号是存储到二进制文件里还是其附属的调试符号文件,即Debug Symbol (dSYM) file,取决于build setting中调试信息格式(DEBUG_INFORMATION_FORMAT)的设定。默认情况是一个应用程序debug的build会存储到编译的二进制文件中,当Release的时候会存储调试符号到一个附属的dSYM文件来减轻二进制文件的大小。

Debug Symbol file和应用程序的二进制文件是通过预构建基础(per-build-basis)的构建UUID来绑定的。每次构建应用程序都会生成一个新的UUID,来唯一确定这次构建。甚至使用相同的编译器设定,对同样的源代码,来进行功能上一模一样的执行重新构建,也会有不同的构建UUID。来自本构建的Debug Symbol文件,尽管是相同的源文件,也不会和其他构建的二进制文件互通。

  1. 当你archive要发布应用程序时,Xcode会收集应用程序二进制文件以及.dSYM文件,然后把它们存储到你的home文件夹的一个地方。你可以在Xcode Organizer的"Archived" 部分中,找到所有你archived的应用程序。

Important:要符号化来之测试者,app审核以及用户的崩溃报告,你必须保存每次你发布应用程序的archive。

  1. 当你通过App Store发布你的App,或者通过TestFlight发起一次beta test,会有当上传你的Archive到iTunes Connect,包含dSYM文件的选项。在提交对话框中,勾选 “Include app symbols for your application…”。上传你的dSYM文件对于接收来自TestFlight用户和选择了分享诊断数据的顾客(即app store用户)的崩溃报告是必要的。

Important:来自App Review的崩溃报告会没法符号化,尽管当你上传到iTunes Connect的archive包含了dSYM文件。你需要通过Xcode来符号化来自App Review的崩溃报告。

  1. 当你的应用程序崩溃,一个未符号化的崩溃报告会产生并存储到设备上。

  2. 通过 Debugging Deployed iOS Apps
    步骤,用户可以直接从他们的设备获取的崩溃报告。如果你是通过AdHoc发布你的应用程序,或者是企业发布(Enterprise distribution),这是唯一从你的用户获取崩溃报告的方法。

  3. 从一个设备获取的崩溃报告是没有被符号化的,需要使用Xcode来进行符号化。Xcode使用应用程序二进制文件关联的dSYM文件,把回溯中的每个地址替换为你源代码原来的位置。替换的结果就是符号化的崩溃报告。

  4. 如果用户选择分享诊断数据给Apple,或者如果用户通过TestFlight安装了你应用程序的一个beta版本,崩溃报告会被上传到App Store。

  5. App store符号化那些崩溃报告,把相似的崩溃报告分组。这些全部相似崩溃报告,被称为崩溃点(Crash Point)。

  6. 已经符号化的崩溃报告可以在Xcode的Crashes organizer中找到和使用。

Bitcode

Bitcode 是代表一个已编译的程序的中间媒介。当Bitcode开启的时候,你archive一个应用程序,编译器产生包含bitcode二进制文件而不是机器码。一旦二进制已经上传到App Store,bitcode会被编译成机器码。App Store 可能在以后再次编译这个bitcode,用于未来编译器的提升,你这边不需要做任何事。

Figure 2:Bitcode编译过程。

Bitcode编译过程

因为你的二进制的最后编译是由App Store完成的,你的Mac不会包含dSYM文件,来符号化来自App Store崩溃报告或者用户从它们设备获取发送给你的崩溃报告。尽管当你archive你的App,会产生dSYM文件,但它是属于bitcode二进制文件的,不能用于符号化崩溃报告。App Store在进行bitcode编译会产程dSYM文件,你可以通过Xcode或者iTunes connect网页进行下载。你必须下载这些dSYM文件,才能符号化来自App Store崩溃报告或者用户从它们设备获取发送给你的崩溃报告。从崩溃报告服务获取的崩溃报告会被自动符号化。

Important: App Store编译的二进制文件的UUID会和你原来提交的二进制文件的UUID不同。

从Xcode下载dSYM文件
  1. 在 Archives organizer选项,选择你原来提交到App Store的archive。
  2. 点击dSYMs按钮来下载。
    Xcode下载dSYM文件并把它们插入到被选择的archive。
从iTunes connect网页下载dSYM文件
  1. 打开App详情页。
  2. 点击活动。
  3. 从所有Builds列表中,选择一个版本。
  4. 点击Download dSYM链接。
把'hidden'符号名翻译它们原来的名字

当你上传你的bitcode App到App Store,你可以在提交对话框里,不勾选选择"Upload your app's symbols to receive symbolicated reports from Apple",从而不发送应用程序的symbols。如果你选择不发送的App的符号信息给Apple,在发送你的App到iTunes Connect之前,Xcode会用一个混淆的symbols,如 "__hidden#109_"来代替你App的symbols。Xcode会创建原始symbols和"hidden" symbols的映射关系并存储这个映射关系到一个. bcsymbolmap文件到这个应用程序的archive中,每一个.dSYM文件都有对应的一个.bcsymbolmap文件。

在符号化崩溃报告之前,你需要反向混淆(还原)这个从iTunes Connect下载的文件。如果你使用Xcode来下载dSYM文件,会为你自动进行反向混淆。然而,如果你使用iTunes Connect网址下载的.dSYM文件,打开终端并使用下面的命令行来反向混淆你的symbols(使用你自己的archive 和从iTunes connect下载的dSYMs文件夹来代替下面范例)。

xcrun dsymutil -symbol-map ~/Library/Developer/Xcode/Archives/2017-11-23/MyGreatApp\ 11-23-17\,\ 12.00\ PM.xcarchive/BCSymbolMaps ~/Downloads/dSYMs/3B15C133-88AA-35B0-B8BA-84AF76826CE0.dSYM

你下载的dSYMs文件夹中的每一个. dSYM文件需要运行这个命令。

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

一个崩溃报告可能未被符号化,完全符号化,或者部分符号化。未被符号化的崩溃报告在回溯中不会包含方法或者函数名。取而代之的是已加载二进制镜像的可执行代码的16进制地址。在一个完全符号化的崩溃报告,回溯的每一行16进制地址都会被对应的符号代替。在部分符号化的崩溃报告,只有部分地址被对应的符号代替。

显然,你应该试着完全符号化崩溃报告,以便更深入地分析崩溃。一个部分符号化的崩溃报告,也许能包含足够信息来明白崩溃,这取决崩溃的类型以及哪部分回溯被成功符号化。一个未被符号化的崩溃报告很少被使用。
Figure 3: 同样的回溯在不同程度的符号化。

不同程度的符号化

使用Xcode符号化崩溃报告

Xcode 会自动尝试符号化它遇到的所有崩溃报告。你只需要将崩溃报告添加到Xcode Organizer。

Note: Xcode不会接收不是以.crash为扩展名的崩溃报告。如果你接收到一个崩溃报告没有扩展名或者扩展名是.txt的,在进行以下步骤之前请将它重命名.crash扩展名。

  1. 将你的Mac和一个iOS设备连接。
  2. 从"Window"菜单,选择"Device"。
  3. 在"DEVICES" 部分的左边栏,选择一个设备。
  4. 点击右手边板面"Device Information"部分的"View Device Logs"按钮。
  5. 将崩溃报告拖到左边栏当前的面板
  6. Xcode会自动符号化这个崩溃报告,并显示结果。

要符号化一个崩溃报告,Xcode必须能找到以下信息:

  • 崩溃应用程序的二进制文件和dSYM文件
  • 所有这个应用程序使用的自定义framework的二进制和dSYM文件。如果frameworks是编译来自应用程序的源代码,它们的dSYM文件会和应用程序的dSYM文件被一起复制进archive。如果frameworks是第三方编译的,你需要向其作者获取dSYM文件。
  • 系统符号是应用程序运行崩溃的所在的系统。这些symbols包含frameworks的调试信息中,包括了一个指定的发布系统信息(如iOS9.3.3)。系统符号(OS symbols)是指定架构的-一个64位设备的iOS发布系统不会包含armv7的符号。Xcode会自动复制从连接你Mac的每个设备中自动复制系统符号。
    如果任意这些信息丢失,Xcode可能无法符号化崩溃报告,或者只能部分符号化崩溃报告。
使用atos符号化崩溃报告

atos命令转换数字地址到它们等价的符号。如果调试符号信息能够使用,atos的输出包含文件名和源码的行数信息。atos命令能够符号化未符号化或部分符号化的崩溃报告回溯中的单个地址。使用atos符号化崩溃报告的一部分:

  1. 在回溯中找到你想符号化的一行。注意二进制镜像的名字在第二列,而地址在第三列。
  2. 找到名字在崩溃报告底部的二进制镜像列表中的二进制镜像。注意二进制的架构和加载地址。
    Figure 4:使用atos所需要的崩溃报告信息。
    崩溃报告信息
  3. 找到二进制文件对应的dSYM文件。你可以使用Spotlight找到匹配dSYM文件UUID的二进制镜像。(可以参考下面部分的:符号化的疑难解答)dSYM文件是包含了编译器在构建时候产生的包含DWARF调试信息文件的集合(bundles)。当调用atos的时候,你必须提供这个文件的路径,而不是aSYM集合。
  4. 有了以上信息,你可以使用atos符号化回溯中的地址。你可以指定多行信息地址,用空格来分隔,来符号化多个地址。
    atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>

Listing 1:按照上面步骤使用atos命令,以及输出的结果。

$ atos -arch arm64 -o TheElements.app.dSYM/Contents/Resources/DWARF/TheElements -l 0x1000e4000 0x00000001000effdc
-[AtomicElementViewController myTransitionDidStop:finished:context:]
符号化的疑难解答

如果Xcode不能完全符号崩溃报告,很可能是因为你的Mac没有应用程序二进制对应的aSYM文件,应用程序关联的Framework的对应的dSYM文件,或者应用程序运行崩溃所在的设备系统的symbols。下面的步骤展示怎样使用Spotlight来确定用于符号化回溯对应二进制镜像的dSYM文件是否存在在你的Mac上。

Figure 5:找到二进制镜像的UUID。

UUID

  1. 找到回溯中Xcode无法符号化的一行。注意二进制镜像的名字在第二列。
  2. 找到名字在崩溃报告底部的二进制镜像列表中的二进制镜像。这个列表包含了崩溃发生时,每个被加载处理的镜像的UUID。
    Listing 2:你可以使用grep命令行工具来快速找到二进制镜像列表的入口。
$ grep --after-context=1000 "Binary Images:" <Path to Crash Report> | grep <Binary Name>
  1. 将二进制镜像的UUID转化成一个被分割成8-4-4-4-12(XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)组的32字节长字符串。注意所有字母必须是大写的。

  2. 使用mdfind命令行工具和"com_apple_xcode_dsym_uuids == <UUID>"(包含引号)查询来UUID。
    Listing 3使用mdfind命令行工具来查找UUID对应的dSYM文件。

$ mdfind "com_apple_xcode_dsym_uuids == <UUID>"
  1. 如果Spotlight找到一个UUID的一个dSYM文件,mdfind会打印dSYM文件的路径以及可能会打印它包含的archive。如果对应UUID的dSYM没有找到,mdfind不会打印任何东西并退出。

如果Spotlight找到二进制对应的dSYM文件,但是Xcode根据那二进制镜像还是不能符号化地址,你应该提交一个bug。将崩溃报告和相关的dSYM文件附带至bug报告中。一个变通的办法,安装上面的介绍的方法使用atos手动符号化地址。

如果Spotlight没有找到二进制对应的dSYM文件,确定你有导致崩溃应用程序版本的Xcode archive,而这个archive处于Spotlight可以找到的地方(应该在你的home目录中)。如果你的应用程序开启bitcode,请确保你已经从App Store下载了最后编译的dSYM文件。

如果你认为你有正确对应二进制文件的dSYM文件,你可以使用dwarfdump命令进行打印匹配的UUIDs。你也可以使用dwarfdump命令来打印二进制文件的UUIDs。
xcrun dwarfdump --uuid <Path to dSYM file>

Note:你必须有提交到崩溃的App Store版本的应用程序的archive。dSYM文件和应用程序的二进制是在预构建基础上绑定在一起的。尽管使用相同的源码和构建配置进行新的archive,也不会产生一个和这个崩溃build互通的dSYM文件。

如果你已经没有这个archive了,你应该提交一个保存了archive的新版本的应用程序,然后你就能够符号化这个新版本的崩溃。

分析崩溃报告

这部分将讨论一个标准崩溃报告的每一部分。

头部

每个崩溃报告都从头部开始
Listing 4:一个崩溃报告的头部

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:一个报告的唯一ID。两个报告不可能使用相同的ID。
  • CrashReporter Key:每个设备的隐藏ID。两个报告来自相同设备会有相同的CrashReporter Key
  • Beta Identifier:设备和崩溃应用提供商组合的唯一码。两个应用程序的报告如果来至相同的提供商和相同的设备会有相同的Beta Identifier。这个只有展示通过TestFlight发布的应用程序产生的崩溃报告,用它来代替CrashReporter Key
  • Process:崩溃进程的可执行的名字。它和应用程序信息属性列表对应CFBundleExecutable键的值。
  • Version:崩溃进程的版本。这个字段的值是崩溃应用程序的CFBundleVersionCFBundleVersionString串联。
  • Code Type:崩溃进程的target的架构。它会是ARM-64, ARM, x86-64, or x86的其中一个。
  • Role:终止时候进程被分配的任务角色
  • OS Version:崩溃发生的系统版本,包括build号。
异常信息

不要被Objective-C/C++的异常迷惑了(尽管其中一个可能导致崩溃),这部分列出提供崩溃本质的信息的匹配异常类型(Exception Type)和相关字段。 不是所有字段都会出现在每一次崩溃。
Listing 5: 因为未知Objective-C异常而进程被终止产生的崩溃报告,其中异常代码的摘录。

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

Listing 6:因为取一个空指针的值(dereferenced a NULL pointer)而进程被终止产生的崩溃报告,其中异常代码的摘录。

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位16进制数字的异常的指定信息。通常来说,这个字段不会显示因为崩溃报告者会解析这些异常代码成人可读的描述来展示到其他的字段。
  • Exception Subtype:人可读的异常代码的名字。
  • Exception Message:从异常代码中解压得到的额外人类可读的信息。
  • Exception Note:非指定一个异常类型的额外信息。如果这个字段包含模拟的(SIMULATED,不是一个崩溃),那么进程不会崩溃,但被系统请求杀死,通常是看门狗。
  • Termination Reason:当进程被终止时指定的退出理由信息。从一个进程的外部和内部看,都是系统的关键组件。遭遇到致命错误(如错误的代码签名,依赖库的丢失,或者没有合适的授权去获取隐私的敏感信息)时会终止进程。macOS Sierra, iOS 10, watchOS 3, and tvOS 10采用了新的基础架构来记录这些错误和这些操作系统产生的崩溃报告会在Termination Reason字段列出。
  • Triggered by Thread:导致异常原因所处的线程。
    下面的部分解释一些最常见的异常类型:
错误的内存访问 [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]

这个进程尝试去访问无效的内存。或者它尝试使用一个内存保护级别不允许的方式去访问内存(例如写入一个只读的内存)。Exception Subtype字段包含一个kern_return_t值来描述错误和不正确访问的内存的地址。
以下是调试错误访问内存访问崩溃的提示:

  • 如果objc_msgSend或者objc_release是在崩溃线程的回溯顶部附近,进程可能是尝试对一个已销毁的对象发送消息。你应该用Zombies instrument
    来profile这个应用程序,以便更好地了解崩溃的条件。
  • 如果gpus_ReturnNotPermittedKillClient是在崩溃线程的回溯顶部附近,进程被杀是因为它尝试在后台使用OpenGL ES或者Metal来绘制。参考:QA1766: How to fix OpenGL ES application crashes when moving to the background
  • 开启Address Sanitizer来运行你的应用程序。address sanitizer(地址消毒剂,实际就是地址检测,能检测内存的异常)为你编译的代码中添加额外的内存访问检测。当你应用程序运行时,如果内存访问方式能导致崩溃,Xcode会警告你。
异常(Abnormal)退出 [EXC_CRASH // SIGABRT]

进程异常退出了。通常导致这种异常类型崩溃的绝大多数原因是未捕获的Objective-C/C++的异常,调用了abort()
App Extensions如果花费太多时间初始化,就会因为这种异常类型而被终止(一种看门狗的终止)。如果一个extension因为启动时挂起被杀死,产生的崩溃报告的Exception Subtype字段值会是LAUNCH_HANG。因为extensions没有main函数,初始化的任何时间都发生在你的extension及其依赖库中的静态构造函数和+load方法中。你应该尽可能地推迟这部分工作。

陷阱追踪 [EXC_BREAKPOINT // SIGTRAP]

和异常退出类似,此异常旨在使附加调试器有机会在执行过程中的某个特定点中断进程。你可以在你的代码中使用__builtin_trap() 函数来触发这种异常。如果没有附加调试器,进程会被终止并产生一个崩溃报告。
低级别的库(例如libdispatch)遇上一个致命错误的会捕获进程。关于错误的额外信息能够在崩溃报告的Additional Diagnostic Information(额外诊断信息,下文有介绍)部分或者设备的控制台中找到。

SWIFT代码如果在运行时遇到意外情况,将以该异常类型终止,如:

  • 不可选类型是nil
  • 强制类型转换失败
    观察回溯来确定意外条件出现在哪里。 额外的信息也可能在设备控制台中打印出来。你必须修改崩溃地方的代码以优雅地处理运行时的错误。例如,使用对一个可选值使用可选绑定来代替强制解包。
非法指令[EXC_BAD_INSTRUCTION // SIGILL]

进程尝试去执行一个非法或者未定义的指令。进程由于错误配置的函数指针跳到一个无效地址。
在英特尔处理器,ud2操作码会导致EXC_BAD_INSTRUCTION异常,但它通常用于捕捉进程的调试目的。在英特尔处理器中,Swift代码如果在运行时一个意外条件触发了,会以这种异常类型终止。细节请参看陷阱追踪。

停止[SIGQUIT]

该进程在另一个具有管理其生命周期的进程的请求下被终止。SIGQUIT不意味着这个进程崩溃,但可能确实是被检测到行为不端。

在iOS,键盘extensions如果加载时间太长,会被主App停止。这样崩溃报告中的回溯很可能不会指向对应的代码。最有可能的是,在extension的启动路径上的一些其他代码花费很多时间才完成,但是在时间限制内完成了,而当extension退出时,代码运行到回溯显示的部分。你应该extension以便更好的了解启动的过程哪儿出现大部分的工作量,使用后台线程来执行这些工作量,或者推迟执行工作量(待extension已经加载后)。

被杀死[SIGKILL]

进程因为系统请求被终止。查看Termination Reason字段以便更好了解终止的原因。

Termination Reason字段会包含一个命名空间,后面跟着一段代码。以下代码是基于watchOS的:

终止代码0xc51bad01表示由于在执行后台任务时使用了太多的CPU时间,所以一个watch app被终止。为了解决这个问题,优化执行后台任务的代码以获得更高的CPU效率,或者减少App在后台运行的工作量。

终止代码0xc51bad02表示由于在分配的时间内未能完成后台任务而终止了手表应用程序。要解决此问题,请减少在后台运行时应用程序执行的工作量。

终止代码0xc51bad03表示一个watch app在分配的时间内未能完成后台任务,并且整体系统足够繁忙,以至于App可能没有接收足够CPU时间到执行后台任务。尽管App可以通过减少在后台任务中执行的工作量来避免此问题,但是0xc51bad03并不表示应用程序出错了。更可能的是,由于整体系统负载,App无法完成其工作。

访问非法资源[EXC_GUARD]

进程违反了资源守护策略。系统的库会标记指定的文件描述符为guarded(被守护的),之后对这些描述符的正常操作会触发一个EXC_GUARD异常(当它想要对这些文件描述符进行操作时,系统使用特殊的“守护”私有API)。这可以帮助您快速追踪问题,如关闭由系统库打开的文件描述符。例如,如果一个应用程序关闭了用于访问支持Core Data存储的SQLite文件的文件描述符,那么Core Data将在后面莫名其妙地崩溃。guard异常更快地注意到这些问题,从而使它们更易于调试。

来自较新版本的iOS的崩溃报告在 Exception SubtypeException Message字段中包含了导致EXC_GUARD异常的操作的人为可读的细节, 引起EXC_GUARD异常的操作的人类可读的细节。在来自macOS或较旧版本的iOS的崩溃报告中,该信息被编码为第一个异常代码,作为一个位字段,如下所示:

  • [63:61] - Guard Type(守护类型):被守护资源的类型。值0x2表示资源是一个文件描述符。
  • [60:32] - Flavor:触发违规的条件。
    如果设置了第一位(1 << 0),进程尝试对一个被守护的文件描述符调用close()。
    如果设置了第二位(1 << 1),进程尝试对一个被守护的文件描述符使用F_DUPFDF_DUPFD_CLOEXEC命令调用dup()dup2()fcntl()
    如果设置了第三位(1 << 2),进程尝试通过socket发送一个被守护文件标识符。
    如果设置了第五位(1 << 4),进程尝试写入一个被守护文件描述符。
  • [31:0] - File Descriptor:进程尝试修改的被守护文件的描述符。
资源限制 [EXC_RESOURCE]

该进程超出了资源消耗限制。这是来自操作系统的通知,该进程正在使用太多的资源。具体的资源列在Exception Subtype字段中。如果 Exception Note字段包含NON-FATAL CONDITION,那么即使生成崩溃报告,该过程也不会被终止。

异常子类型MEMORY表示该进程已超过系统强加的内存限制。这可能是终止过量内存使用的先兆。

异常子类型WAKEUPS表示进程中的线程每秒被唤醒的次数太多,这迫使CPU醒来的频率很高,并消耗电池寿命。
通常,这是由线程间通信(通常使用peformSelector:onThread:dispatch_async)导致的,这些通信不知不觉地发生得比正常情况频繁得多。因为触发这种异常的通信发生得非常频繁,所以通常会有多个具有非常相似的回溯的后台线程 - 指示通信起源的位置。

其他异常类型

某些崩溃报告可能包含一个未命名的异常类型,将以十六进制值打印(例如00000020)。如果您收到一个这种崩溃报告,请直接查看Exception Codes字段以获取更多信息。

  • 异常代码0xbaaaaaad表示日志是整个系统的堆栈快照,而不是崩溃报告。要拍摄堆栈快照,请同时按侧面按钮和两个音量按钮。这些日志通常是由用户意外创建的,并不表示一个错误。
  • 异常代码0xbad22222表示一个VoIP应用程序已被iOS终止,因为它恢复得太频繁。
  • 异常代码0x8badf00d表示应用程序因为看门狗超时已被iOS终止。应用程序启动,终止或响应系统事件花了太长时间。一个常见的原因是在主线程上进行同步网络操作。无论是什么操作,线程0都需要移动到后台线程,或者以不同的方式进行处理,这样就不会阻塞主线程。
  • 异常代码0xc00010ff表示该App因响应热事件(如CPU温度过高)而被操作系统杀死。这可能是由于发生此次崩溃的特定设备或其所在的环境所引起的问题。有关使你的App更高效运行的提示,请参阅iOS Performance and Power Optimization with Instruments WWDC session。
  • 异常代码0xdead10cc表示应用程序已被操作系统终止,因为它在挂起期间持有文件锁或sqlite数据库锁。如果您的应用程序在挂起时对锁定了的文件或sqlite数据库执行操作,则必须请求额外的后台执行时间才能完成这些操作,并在挂起之前放弃锁定。
  • 异常代码0x2bad45ec表示应用程序由于安全违规而被iOS终止。终止描述“进程被检测到在安全模式下进行不安全的绘图”表示App尝试在不允许的情况下绘制到屏幕上,例如屏幕锁定的时候。用户可能不会注意到这个终止,因为屏幕关闭或终止时显示的是锁定屏幕。

Note: 使用App切换器终止一个挂起的程序不会产生崩溃报告。一旦程序挂起了,它随时有可能被iOS终止,所有不会产生崩溃报告。

额外诊断信息

这部分包含特定于终止类型的额外诊断信息,其中可能包括:

应用程序的特定信息:在进程终止之前捕获的框架错误消息
内核消息:有关代码签名问题的详细信息
Dyld错误消息:由动态链接器发出的错误消息

dyld 是Apple 的动态链接器;在 xnu 内核为程序启动做好准备后,就会将 PC 控制权交给 dyld 负责剩下的工作 (dyld 是运行在 用户态的, 这里由 内核态 切到了用户态)。

从macOS Sierra,iOS 10,watchOS 3和tvOS 10开始,大部分信息现在都在异常信息下的Termination Reason字段中报告。

您应该阅读本节以更好地了解进程终止的情况。
Listing 7:一个进程因为一个框架的链接无法找到而被终止的崩溃报告的应用程序特定信息部分的摘录。

Dyld Error Message:
Dyld Message: Library not loaded: @rpath/MyCustomFramework.framework/MyCustomFramework
  Referenced from: /private/var/containers/Bundle/Application/CD9DB546-A449-41A4-A08B-87E57EE11354/TheElements.app/TheElements
  Reason: no suitable image found.

Listing 8: 一个进程因为无法快速加载其初始视图控制器而被终止的崩溃报告的应用程序特定信息部分的摘录。

Application Specific Information:
com.example.apple-samplecode.TheElements failed to scene-create after 19.81s (launch took 0.19s of total time limit 20.00s)
 
Elapsed total CPU time (seconds): 7.690 (user 7.690, system 0.000), 19% CPU
Elapsed application CPU time (seconds): 0.697, 2% CPU
回溯

崩溃报告中最有趣的部分是每个进程的线程在其终止时的回溯。 这每一个跟踪与您在使用调试器暂停过程时所看到的类似。
Listing 9:完全符号化的崩溃报告中回溯部分的摘录。

Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   TheElements                     0x000000010006bc20 -[AtomicElementViewController myTransitionDidStop:finished:context:] (AtomicElementViewController.m:203)
1   UIKit                           0x0000000194cef0f0 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 312
2   UIKit                           0x0000000194ceef30 -[UIViewAnimationState animationDidStop:finished:] + 160
3   QuartzCore                      0x0000000192178404 CA::Layer::run_animation_callbacks(void*) + 260
4   libdispatch.dylib               0x000000018dd6d1c0 _dispatch_client_callout + 16
5   libdispatch.dylib               0x000000018dd71d6c _dispatch_main_queue_callback_4CF + 1000
6   CoreFoundation                  0x000000018ee91f2c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
7   CoreFoundation                  0x000000018ee8fb18 __CFRunLoopRun + 1660
8   CoreFoundation                  0x000000018edbe048 CFRunLoopRunSpecific + 444
9   GraphicsServices                0x000000019083f198 GSEventRunModal + 180
10  UIKit                           0x0000000194d21bd0 -[UIApplication _run] + 684
11  UIKit                           0x0000000194d1c908 UIApplicationMain + 208
12  TheElements                     0x00000001000653c0 main (main.m:55)
13  libdyld.dylib                   0x000000018dda05b8 start + 4
 
Thread 1:
0   libsystem_kernel.dylib          0x000000018deb2a88 __workq_kernreturn + 8
1   libsystem_pthread.dylib         0x000000018df75188 _pthread_wqthread + 968
2   libsystem_pthread.dylib         0x000000018df74db4 start_wqthread + 4
 
...

第一行列出当前正在执行的调度队列的线程号和标识符。其余行列出了回溯中各个堆栈帧的详细信息。从左到右:

  • 堆栈帧号。堆栈帧以调用顺序呈现,其中第0帧是执行停止时正在执行的函数。第1帧是调用第0帧的函数,依此类推。
  • 堆栈帧的执行函数所在的二进制文件的名称。
  • 对于第0帧,执行暂停时正在执行的机器指令的地址。对于剩余的堆栈帧,当控制权返回到堆栈帧时,将执行的机器指令的地址。
  • 在已符号化的崩溃报告中,函数的方法名称就在堆栈帧中。
异常

Objective-C中的异常用于指示在运行时检测到的编程错误,例如访问具有超出边界的索引的数组,尝试修改不可变对象,不执行协议必须的方法或发送接收器不能识别的消息。

Note:对已销毁的对象的发送消息可能会导致NSInvalidArgumentException异常,而不是因为非法访问内存而导致程序崩溃。当一个新对象分配内存,恰好被分配到先前被释放对象占用的内存中时,会发生这种情况。如果您的应用程序是因未捕获的NSInvalidArgumentException(在异常回溯中查找 - [NSObject(NSObject)doesNotRecognizeSelector:])而崩溃,请考虑使用僵尸工具(Zombies instrument)来profile您的应用程序,以消除不正确的内存管理的可能性。

如果一个异常没有被捕获,则被一个称为未捕获的异常处理函数(uncaught exception handler)的函数截获。默认的未捕获异常处理程序将异常消息记录到设备的控制台,然后终止该进程。只有异常回溯写入到生成的崩溃报告中的Last Exception Backtrace部分下,如Listing 10所示。崩溃报告中省略了异常消息。如果您收到带有Last Exception Backtrace的崩溃报告,则应从原始设备获取控制台日志,以便更好地了解导致该异常的条件。
Listing 10:一个未符号化的崩溃报告的Last Exception Backtrace部分的摘录。

Last Exception Backtrace:
(0x18eee41c0 0x18d91c55c 0x18eee3e88 0x18f8ea1a0 0x195013fe4 0x1951acf20 0x18ee03dc4 0x1951ab8f4 0x195458128 0x19545fa20 0x19545fc7c 0x19545ff70 0x194de4594 0x194e94e8c 0x194f47d8c 0x194f39b40 0x194ca92ac 0x18ee917dc 0x18ee8f40c 0x18ee8f89c 0x18edbe048 0x19083f198 0x194d21bd0 0x194d1c908 0x1000ad45c 0x18dda05b8)

一个崩溃日志的Last Exception Backtrace部分只包含十六进制地址,必须符号化以产生可用的回溯,如Listing 11所示。

Listing 11:一个已符号化的崩溃报告的Last Exception Backtrace部分的摘录。 这是在加载storyboard一个scene时引发了此异常。 连接到scene中一个元素的对应IBOutlet丢失了。

Last Exception Backtrace:
0   CoreFoundation                  0x18eee41c0 __exceptionPreprocess + 124
1   libobjc.A.dylib                 0x18d91c55c objc_exception_throw + 56
2   CoreFoundation                  0x18eee3e88 -[NSException raise] + 12
3   Foundation                      0x18f8ea1a0 -[NSObject(NSKeyValueCoding) setValue:forKey:] + 272
4   UIKit                           0x195013fe4 -[UIViewController setValue:forKey:] + 104
5   UIKit                           0x1951acf20 -[UIRuntimeOutletConnection connect] + 124
6   CoreFoundation                  0x18ee03dc4 -[NSArray makeObjectsPerformSelector:] + 232
7   UIKit                           0x1951ab8f4 -[UINib instantiateWithOwner:options:] + 1756
8   UIKit                           0x195458128 -[UIStoryboard instantiateViewControllerWithIdentifier:] + 196
9   UIKit                           0x19545fa20 -[UIStoryboardSegueTemplate instantiateOrFindDestinationViewControllerWithSender:] + 92
10  UIKit                           0x19545fc7c -[UIStoryboardSegueTemplate _perform:] + 56
11  UIKit                           0x19545ff70 -[UIStoryboardSegueTemplate perform:] + 160
12  UIKit                           0x194de4594 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1352
13  UIKit                           0x194e94e8c -[UITableView _userSelectRowAtPendingSelectionIndexPath:] + 268
14  UIKit                           0x194f47d8c _runAfterCACommitDeferredBlocks + 292
15  UIKit                           0x194f39b40 _cleanUpAfterCAFlushAndRunDeferredBlocks + 560
16  UIKit                           0x194ca92ac _afterCACommitHandler + 168
17  CoreFoundation                  0x18ee917dc __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
18  CoreFoundation                  0x18ee8f40c __CFRunLoopDoObservers + 372
19  CoreFoundation                  0x18ee8f89c __CFRunLoopRun + 1024
20  CoreFoundation                  0x18edbe048 CFRunLoopRunSpecific + 444
21  GraphicsServices                0x19083f198 GSEventRunModal + 180
22  UIKit                           0x194d21bd0 -[UIApplication _run] + 684
23  UIKit                           0x194d1c908 UIApplicationMain + 208
24  TheElements                     0x1000ad45c main (main.m:55)
25  libdyld.dylib                   0x18dda05b8 start + 4

Note:如果您发现由应用程序设置的异常处理域内引发的异常未被捕获,请验证在构建应用程序或库时是否未指定-no_compact_unwind标志。

64位iOS使用“zero-cost(零成本)”异常实现。在“zero-cost”系统中,每个函数都有额外的数据,用于描述如何在函数中抛出异常时展开堆栈。如果在没有展开数据的堆栈帧中抛出异常,则异常处理无法执行,并且进程中止。在堆栈上可能有一个异常处理程序,但是如果一个框架没有展开数据,那么引发异常时无法从堆栈帧到达那里。指定-no_compact_unwind标志意味着你没有得到那个代码的展开表,所以你不能在这些函数中抛出异常。

此外,如果您在应用程序或库中包含纯C代码,则可能需要指定-funwind-tables标志以包含该代码中所有函数的展开表。

线程状态

这部分列出崩溃的线程的线程状态。这是执行中止时的寄存器及其值的列表。在阅读崩溃报告时,不一定要明白线程状态,但是你可以使用这些信息来更好地理解崩溃的条件。

Listing 12:来自ARM64设备的崩溃报告中线程状态部分的摘录。

Thread 0 crashed with ARM Thread State (64-bit):
    x0: 0x0000000000000000   x1: 0x000000019ff776c8   x2: 0x0000000000000000   x3: 0x000000019ff776c8
    x4: 0x0000000000000000   x5: 0x0000000000000001   x6: 0x0000000000000000   x7: 0x00000000000000d0
    x8: 0x0000000100023920   x9: 0x0000000000000000  x10: 0x000000019ff7dff0  x11: 0x0000000c0000000f
   x12: 0x000000013e63b4d0  x13: 0x000001a19ff75009  x14: 0x0000000000000000  x15: 0x0000000000000000
   x16: 0x0000000187b3f1b9  x17: 0x0000000181ed488c  x18: 0x0000000000000000  x19: 0x000000013e544780
   x20: 0x000000013fa49560  x21: 0x0000000000000001  x22: 0x000000013fc05f90  x23: 0x000000010001e069
   x24: 0x0000000000000000  x25: 0x000000019ff776c8  x26: 0xee009ec07c8c24c7  x27: 0x0000000000000020
   x28: 0x0000000000000000  fp: 0x000000016fdf29e0   lr: 0x0000000100017cf8
    sp: 0x000000016fdf2980   pc: 0x0000000100017d14 cpsr: 0x60000000
二进制镜像

本部分列出终止时进程加载的二进制映像。
Listing 13:在崩溃报告中二进制镜像部分中,应用程序的入口摘录。

Binary Images:
0x100060000 - 0x100073fff TheElements arm64 <2defdbea0c873a52afa458cf14cd169e> /var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
...

了解低内存报告

了解低内存报告
当检测到低内存条件时,iOS中的虚拟内存系统依靠应用程序的协作来释放内存。低内存通知发送到所有正在运行的应用程序和进程作为一个释放内存的请求,希望减少内存的使用。如果内存压力仍然存在,系统可能会终止后台进程以减轻内存压力。如果有足够的内存被释放,你的应用程序将继续运行。否则,您的应用程序将被iOS终止,因为没有足够的内存来满足应用程序的需求,而低内存报告会被生成并存储在设备上。

低内存报告的格式不同于其他崩溃报告,因为没有应用程序线程的回溯。低内存报告与崩溃报告的头部(Header)相似。下面的头部是列出系统范围内存统计信息的字段集合。记下Page Size字段的值。低内存报告中每个进程的内存使用情况以内存页面数量的形式报告。

低内存报告中最重要的部分是进程表。此表列出了生成低内存报告时所有正在运行的进程,包括系统守护进程。如果一个进程被“抛弃”,原因将被列在 [reason] 栏中。一个进程可能会被抛弃的原因有很多:

  • [per-process-limit]:该进程超过了系统强加的内存限制。系统为所有应用程序中的每个进程建立驻留内存的限制。超过此限制使得流程符合终止条件。

Note:Extensions每个进程的内存限制要低得多。某些技术(如map views和SpriteKit)具有较高的基线内存消耗,可能不适合用于Extensions。

  • [vm-pageshortage] / [vm-thrashing] / [vm]:由于内存压力,进程被终止。
  • [vnode-limit]:打开的文件过多。

Note:当vnodes(虚拟节点,系统中数据分片的单位)快用完时,系统可以避免杀死最前面的应用程序。这意味着当您的应用程序处于后台,即使它不是使用多余的vnode的源头,也可能被终止。

  • [highwater]:系统守护进程超过了内存使用的高位标记。
  • [jettisoned]:这个进程因为其他原因被抛弃(终止)了。
    如果在app/extension没有列出原因,那么崩溃的原因不是内存压力。查找一个.crash文件(在上一节中介绍)以获取更多信息。

当你看到一个内存不足的崩溃时,你应该调查你的内存使用模式和你对低内存警告的应对,而不是关心终止时正在执行什么代码。在应用程序中查找内存问题(Locating Memory Issues in Your App)列出了如何使用泄漏工具(Leaks Instrument)发现内存泄漏的详细步骤,以及如何使用分配工具的标记堆(Allocations Instrument's Mark Heap)功能来避免废弃的内存。 内存使用性能指南讨论了响应低内存通知的正确方法,以及有效使用内存的许多技巧。还建议您查看WWDC 2010 session,使用工具进行高级内存分析(Advanced Memory Analysis with Instruments)

Important:Leaks(泄漏)和Allocations(分配)工具不会跟踪所有内存使用情况。您需要使用VM Tracker工具(包含在工具分配的模板中)运行您的应用程序以查看您的总内存使用情况。VM Tracker默认是禁用的。要使用VM跟踪器来分析您的应用程序,请单击该工具,检查“Automatic Snapshotting(自动快照)”标志或手动按下“Snapshot Now(快照)”按钮。

相关文档

有关如何使用工具僵尸模板修复关于内存过度释放的崩溃的信息,请参阅Eradicating Zombies with the Zombies Trace Template

有关应用程序archiving的更多信息,请参阅App Distribution Guide

有关解析崩溃日志的更多信息,请参阅Understanding Crash Reports on iPhone OS WWDC 2010 Session

推荐阅读更多精彩内容