WeTest 导读node
北京时间凌晨一点,苹果一年一度的发布会如期而至。新机型的发布又会让适配相关的同窗忙上一阵子啦,而且iOS Crash的问题始终伴随着移动开发者。本文将从三个阶段,由浅入深的介绍如何看懂并分析一篇crash报告,一块儿身临其境去读懂它吧。程序员
孟嵩:这篇万字长文,大概先后翻译了一个月,“写”了三遍:第一遍是直译,第二遍是把直译改为程序员看着舒服的“行话”,第三遍是把原文里说的过于抽象或者简单的部分加上个人注解(你们看见全部以孟嵩开头的部分)。sql
当app发生crash时会产生crash report,这对咱们定位crash的缘由很是有帮助。该篇重点介绍了如何符号化、看懂并解析一篇crash Report。数据库
孟嵩:数组
开篇给出了这个文档的三个阶段,由浅入深为:xcode
1. 符号化,把不可读的文档转成可读安全
2. 看懂,意思就是知道文档里哪一个部分表达的什么网络
3. 解析,意思就是能从文档中定位问题,获取解决问题的有价值的信息。架构
ps:文内展现代码都可左右滑动查看app
Crash Report,尤为是堆栈信息,在被符号化以前是不可读的。所谓符号化就是把内存地址用可读的函数名和行数来替换。若是你不是从设备直接获取的crash日志,而是经过Xcode的Device Window(即经过视图操做而非手动命令行),它们会在几秒以后自动被符号化。固然你也能够把.crash文件加入到Xcode的Device Window并自行将它符号化。
Low Memory Report与其它crash report不一样,它没有堆栈信息。当因为低内存而发生crash时,你必须反思你的内存使用模式和你针对低内存警告的应对方法。本文会提供给你几个内存管理的参考实现,供你参考。
如何调试已经部署好的iOS Apps讨论了如何从一个iOS设备直接拿到crash report和low memory report。
App发布指南里的分析Crash Reports讨论了如何查看那些crash report,这些report既包含经过TestFlight下载的测试用户处得到,又包含经过App Store下载的正式用户处得到。
符号化指的是一种手段,这种手段指的是把堆栈信息(二进制信息)解释成源码里的方法名或者函数名,也就是所谓符号。只有符号化成功后,crash report才能帮助开发者定位问题。
注意:Low Memory Report不须要被符号化(由于没有堆栈信息)。
注意:在MacOS平台上产生的crash report在生成的时候通常都会被彻底符号化过或者半符号化过。所以本节指的符号化针对的是从iOS、watchOS乃至tvOS中提取出来的crash report。总体处理流程上,macOS的carsh report比较相似。
[ crash上报和符号化过程概述 ]
1. 编译器在把你的源代码转换成机器码的同时,也会生成一份对应的Debug符号表。Debug符号表实际上是一个映射表,它把每个藏在编译好的binary信息中的机器指令映射到生成它们的每一行源代码中。经过build setting里的Debug Information Format(DEBUG_INFORMATION_FORMAT),这些Debug符号表要么被存储在编译好的binary信息中,要么单独存储在Debug Symbol文件中(也就是dSYM文件):通常来讲,debug模式构建的app会把Debug符号表存储在编译好的binary信息中,而release模式构建的app会把debug符号表存储在dSYM文件中以节省体积。
在每一次的编译中,Debug符号表和app的binary信息经过构建时的UUID相互关联。每次构建时都会生成新的惟一的可以标识那次构建的UUID,即使你用一样的源代码,经过一样的编译setting,UUID也不会相同。相应的,dSYM文件也不能用于解析其它(UUID对应的)binary信息,即使构建自于同一个源代码。
孟嵩:意思就是说,同一次构建,app+dSYM+UUID是一套的。若是这几个文件不属于同一次构建,即使是相同的源代码,互相之间在符号化这个事情上也没法互相工做。
2. 当你为了分发app而选择Archive(存档)时,Xcode会把app的二进制信息和.dYSM文件存储在你的home文件夹下的某个地方。你能够在Xcode的Organizer里面经过”Archived”选项找到全部你存档过的app。 更多存档app的细节,请点击官方文档-分发你的App一文。
注意:想要解析来自于测试、app review或者客户的crash report,你须要保留分发出去的那些构建过的archive文件。
3. 若是你是经过App Store分发app或者是Test Flight分发的beta版本的app,你将在上传archive到ITC(iTunes Connect)时看见一个“是否将dSYM一块儿上传”的选项。在上传对话框中,请勾选”在app中包含app符号表”。上传你的dYSM文件对于从TestFlight用户和客户以及愿意分享诊断信息的客户那边接收crash report是颇有必要的。更多详情请参考官方文档-分发你的App一文。
注意:接收自App Review的crash report是不会被符号化的,及时你再上传你的app到ITC时勾选了包含dSYM文件。任何来自于App Review的crash report都须要在Xcode里作符号化。
4. 当你的app 发生crash时,一个没有被符号化的crash report会被建立并存储在设备上。
5. 用户能够经过调试已部署的iOS APP里提到的方法来直接从他们的设备里得到crash report。若是你经过AdHoc或者企业证书分发app,这是你惟一能从用户获取crash report的方法。
6. 从设备上直接获取的crash report是没有被符号化的,你须要经过Xcode来符号化。Xcode会结合dSYM文件和你app的二进制信息把堆栈里的每个地址对应到源代码中。处理后的结果就是一个符号化过的crash report。
7. 若是用户愿意和Apple共享诊断信息,或者用户经过TestFlight下载了你的beta版本app,那crash report会被上传到App Store。
8. App Store在符号化crash report后会把内部全部的crash reports作汇总并分组,这种聚合(类似crash report)的方法叫作crash聚类。
9. 这些符号化后的crash report能够在你的Xcode的Crash Organizer中进行查看。
Bitcode(位编码)是一个编译好的项目的中间表现形式。当你在容许bitcode的前提下Archive一个app时,编译器会在二进制中包含bitcode而不是机器码。一旦binary信息被上传到App Store中,bitcode会被再次编译成机器码。也许App Store会在未来二次编译bitcode,例如为提升编译器性能而二次编译等。不过这不重要,由于一切对你来讲是透明的,也就不须要你来额外付出什么。
[ 图2 BitCode编译过程概览 ]
由于你的binary信息的最终编译结果是在App Store上体现的,所以你的Mac将不会包含那些须要对从App Review或者用户的设备那里获取到的Crash report所必须的符号化用的dSYM。
孟嵩:这里原文很拗口,大概意思就是须要的东西都在App Store云端,以后的操做会自动进行,见下文。
虽然当你Archive你的app时会建立dSYM文件,但它们只能用在bitcode binary信息中,并不能用于符号化crash report。 App Store容许你从Xcode或者ITC网站中下载这些随着bitcode编译而产生的dSYM文件。 为了解析从App Review或者给你发送crash report的用户的crash report,你必需要下载这些dSYM文件,这样才能符号化crash report。 若是是从crash reporting service那里接收crash report,符号化会自动完成。
注意:App Store上编译的binary信息和提交的原始文件的UUID是不一样的。
· 在Archives organizer,选择你以前提交到App Store的Archive文件
· 选择Download dSYM按钮Archive
Xcode会下载dSYM文件而且把他们插入到选择的Archive中。
· 打开App详情页面
· 点击 Activity
· 从全部的构建中,选择一个版本
· 点击 下载dSYM文件的连接
当你把一个带有bitcode的app上传到App Store时,你也许在提交对话框中并无勾选“上传你的app的符号表信息以便从Apple那边接收符号化过的 report”的选项。 当你选择不发送符号表信息给Apple时,Xcode会在你发送app到ITC以前用晦涩难懂的符号例如”__hidden#109_”等来替换你的app里的dSYM文件。Xcode会建立一个原始符号和”隐藏”符号的对照表,而且将其存储在Archive的app文件中的一个bcsymbolmap文件里。每个dSYM文件都会有一个对应的bcsymbolmap文件。
在符号化crash report以前,你须要把那些从ITC中下载下来的dSYM文件中的晦涩信息给解析一下。 若是你使用Xcode中的下载dSYM按钮,这步解析会自动完成。可是,若是你经过ITC网站来下载dSYM的话,你须要打开Terminal而且手动输入下面的命令来作解析(把example的path信息和sSYM信息替换一下)
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文件都运行一次这条命令。
一个crash report有可能未符号化,彻底符号化,也有可能部分符号化。未符号化的crash report不会在堆栈信息中包含方法名或者函数名。相反,你会在加载好的binary信息中发现可执行的16进制地址信息。在彻底符号化的crash report里,堆栈中的每一行16进制地址信息都会被替换成对应的符号。在部分符号化的crash report中,只有一部分堆栈信息被替换成相应的符号信息。
显然,你应当尽力去彻底符号化你的crash report,由于那样你才可以得到crash report里最有价值的信息。一个部分符号化的crash report也许包含了能够理解crash的信息,这取决于crash的类型和哪一部分被成功符号化了。一个未符号化的crash report用处有限。
[ 相同堆栈信息下的不一样程度的符号化 ]
Note:Xcode只认.crash后缀的crash report。若是你收到的crash report没有后缀名或者后缀是txt,在执行下列步骤以前先把它改为.crash。
· 把iOS设备链接到你的Mac
· 从Window菜单栏选择Devices
· 在Devices左侧,选择一个设备
· 点击右边在“Device Information“ 下面的 ”View Device Logs” 按钮
· 把你的Crash report拖拽到左侧panel中
· Xcode会自动符号化Crash report而且显示结果
为了符号化一个Crash report,Xcode须要去定位以下信息:
· 崩溃的app的binary信息以及dSYM文件
· 全部app关联的自定义framework的binary信息以及dSYM文件。若是是从app构建出来的framework,它们的dYSM会随着app的dSYM文件一块儿拷贝到archive中。若是是第三方的framework,你须要去找做者要dYSM文件。
· 发生crash时app所依赖的OS的符号表信息。这些符号表包含了特定OS版本
(例如iOS9.3.3)上的framework所需调试信息。 OS 符号表的架构具备独特性——一个64位的iOS设备不会包含armv7的符号表。Xcode将要自动拷贝你链接到的特定版本的Mac的符号表。
在上述任何一处,若是没有Xcode,你将没法符号化一个crash report,或者只能部分符号化一个crash report。
atos命令能够把地址里的数字替换成等价的符号。若是调试符号信息是完备的,则atos的输出信息将会包含文件名和对应的资源行数。atos命令能够被用来单独符号化那些未符号化或者部分符号化过的crash report(中的堆栈信息里的地址)。
想要使用atos符号化crash report能够按以下方式操做:
1. 找到你想要符号化的那一行,记下第二列的binary信息名,以及第三列的地址。
2. 从crash report底部的binary信息名列表中找到那个名字,记下来架构名和加载的地址。
孟嵩:例如在下图里,咱们想符号化的部分就是0x00000001000effdc,binary信息名是The Elements,底部能找到对应的名字的架构名称是arm64,加载地址是0x1000e4000。
[ 在Crash report里提取出使用atos所须要的信息 ]
1. 定位二进制对应的dSYM文件。你能够用Splotlight,结合UUID,来寻找匹配的dSYM文件。(请查看相关章节。)dSYM是一个bundle,包含经过编译器在build时编译出来的DWARF调试信息(nimo: DWARF的可能的解释是,Debugging With Attributed Record Formats,是一种调试文件结构标准,结构至关的复杂)。你在使用atos时必须提供这个文件的路径,而不是dSYM的bundle路径。
2. 有了上述信息以后,你就能够把堆栈里的地址经过atos命令来符号化了。你能够符号化多条地址,经过空格来进行区分。
atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>
清单1 使用atos命令的样例,以及结果输出
$ atos -arch arm64 -o TheElements.app.dSYM/Contents/Resources/DWARF/TheElements -l 0x1000e4000 0x00000001000effdc
-[AtomicElementViewController myTransitionDidStop:finished:context:]
若是Xcode没有彻底符号化一个crash report,极可能是你的Mac丢失了app binary信息对应的dSYM文件,或者是丢了一个或多个app关联的framework的dSYM文件,也有可能在发生Crash时OS层面的app的设备符号表丢失了。下列步骤显示了如何使用Spotlight来判断那些能够符号化对应堆栈地址信息的dSYM文件是否在你的Mac上。
[ 定位一个二进制镜像 ]
1. 在Xcode没法符号化的堆栈里找一行,注意第二列的binary信息的名字。
2. 在crash report的底部中的二进制信息列表里找到那个名字。这个列表包含了每个crash事故现场存在于进程里的二进制信息的UUID。
孟嵩:本例中须要关注的binary信息的名字是The Element,在底部列表中对应的二进制信息的UUID是77b672e2b9f53b0f95adbc4f68cb80d6
列表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信息。
列表3 使用mdfind命令来经过给定UUID查找dSYM文件。
$ mdfind "com_apple_xcode_dsym_uuids == <UUID>"
1. 若是spotlight找到了UUID对应的dSYM文件,mdfind会把dSYM文件和可能包含的归档文件的路径打印出来。若是一个UUID对应的dSYM文件没有找到,mdfind会直接退出。
若是spotlight找到了二进制对应的dSYM文件,可是Xcode没有能结合二进制信息成功把地址符号化,那你应该上报一枚bug而且把crash report和对应的dSYM文件一块儿附到bug report中。做为权宜之策,你能够手动用atos来对地址进行符号化。
若是spotlight没有找到二进制信息对应的dSYM文件,确保你还有app发生crash的那个版本的Xcode归档文件,而且这个文件存在于spotlight能够找到的某个地方。若是你的app是支持bitcode方式构建的,确保你已经从App Store下载了最终编译版本的dSYM文件。
若是你以为你已经有了二进制信息对应的正确的dSYM文件,那你能够用dwarfdump命令来打印对应的匹配UUID。你也能够用用dwarfdump命令来打印二进制的UUID。
xcrun dwarfdump --uuid <Path to dSYM file>
注意:你必须保存你最开始上传到App Store的发生crash的app的归档文件。dSYM文件和app二进制文件是一一对应,且每次构建都不相同。即使经过相同的源码和配置,再执行一次构建,生成的dSYM文件也没法和以前的crash report作符号化匹配。
若是你不在存有这个归档文件,你应该从新提交一次有归档的新版本,以确保再发生crash的时候你能够符号化crash report。
这段将会讨论一篇标准crash report的各章节的含义。
每一篇crash report都有一个header。
列表4 一篇crash report的header部分
Incident Identifier: B6FD1E8E-B39F-430B-ADDE-FC3A45ED368C
CrashReporter Key: f04e68ec62d3c66057628c9ba9839e30d55937dc
Hardware Model: iPad6,8Process: TheElements [303]Path: /private/var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElementsIdentifier: com.example.apple-samplecode.TheElementsVersion: 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: 一个crash report的惟一ID。两个 report不会使用同一个Incident Identifier。
· CrashReporter Key: 一个匿名的设备相关ID。同一个设备的两篇crash report会有相同的CrashReporter Key。
· Beta Identifier:一个整合了发生crash app的设备和供应商信息的ID。来自同一个供应商和设备的两篇report会包含相同的ID值。这个字段只有当app经过TestFlight分发的时候出现,而且出如今应该出现Crash Reporter Key Field的地方。
· Process:发生Crash时的进程名。这个和app信息属性列表里的CFBundleExecutable Key中的值能够匹配上。
· Version:发生crash的版本号。这个值能够关联到发生 crash的app的CFBundleVersion 和 CFBundleVersionString上。
· Code Type:发生crash的上下文所在架构环境。有ARM-64,ARM,X86-64和X86.
· Role:在发生crash时进程的的task_role。
孟嵩:task_role的定义以下:
enum task_role {
TASK_RENICED = -1,
TASK_UNSPECIFIED = 0,
TASK_FOREGROUND_APPLICATION,
TASK_BACKGROUND_APPLICATION,
TASK_CONTROL_APPLICATION,
TASK_GRAPHICS_SERVER,
TASK_THROTTLE_APPLICATION,
TASK_NONUI_APPLICATION,
TASK_DEFAULT_APPLICATION
};
· OS Version: OS version,包含发生crash时的所属app的编译码。
列表5 因为uncaught Objective-C exception而致使的进程被中止的crash report的摘录
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0
列表6 因为反向引用了一个NULL指针而形成进程被终止的crash report的摘录
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位二进制数字。通常来讲,这个字段不该该存在,由于crash report生成时会把exception code转化成可读的信息并在其它字段进行体现。
· Exception Subtype:可读的exception code的名称。
· Exception Message:从exception code中解析出来的附加的可读信息。
· Exception Note:不特指某一种异常的额外信息。若是这个字段包含”SIMULATED”(不是Crash),则进程并无发生crash,而是在系统层面被kill掉了,好比看门狗机制。
孟嵩:为了防止一个应用占用过多的系统资源,苹果工程师门设计了一个“看门狗”的机制。“看门狗”会监测应用的性能。若是超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。
开发者们在crashlog里面,会看到诸如0x8badf00d这样的错误代码(看起来很像bad food,看门狗吃到了坏的食物,不嗨森)。
看门狗触发条件以下:
[ 看门狗触发时机 ]
· Termination Reason:当进程被终止时的缘由及信息。关键的信息模块,不管是进程内仍是进程外,当遇到一个致命错误(fatal error,例如bad code signature,缺失依赖库,不恰当的访问私有敏感信息等)。MacOS Sierra,iOS 10, watch OS3和tvOS 10 已经采用新的架构去记录这些错误信息,因此这些系统之下的crash report会在Termination Reason这个字段里描述error message信息。
· Triggered by Thread:指出异常是在哪一个线程发生的
接下来的章节会解释常见的异常类型:
进程试图去访问无效的内存空间,或者尝试访问的方法是不被容许的(例如给只读的内存空间作写操做)。在Exception Subtype字段中若是出现kern_return_t的话,说明内存地址空间被不正确的访问了。
这里有几个调试bad memory crash的小贴士:
· 若是 objc_msgSend 或者 objc_release出如今crash的线程的附近,则进程有可能尝试去给一个被释放的对象发送消息。你应当用Zombie instrument方式来运行profile,来更好地了解发生crash的缘由。
· 若是gpus_ReturnNotPermittedKillClient在近crash的线程附近,则进程有多是尝试在后台经过OpenGL ES或者Metal来作渲染。能够参见 QA1766: How to fix OpenGL ES application crashes when moving to the background
· 经过在运行你的app时勾选Address Sanitizer。address sanitizer会在编译期间在内存访问时添加额外的操做,当你的app运行,Xcode会在内存可能发生crash的时候给出提示信息。
进程异常退出。这种异常最多见的缘由在于uncaught Objective-C/C++ exception而且调用了abort()。
扩展App(nimo:App Extensions,例如输入法)若是花了太多时间作初始化的话就会以这种异常退出(看门狗机制)。若是扩展程序因为在启动时挂起进而被kill掉,那 report中的Exception Subtype字段会写LAUNCH_HANG。由于扩展App没有main函数,因此任何状况下的在static constructors和+load方法里的初始化时间都会体如今你的扩展或者依赖库中。所以你应当尽量的推迟这些逻辑。
和Abnormal Exit相似,这种异常是因为在特殊的节点加入debugger调试节点的缘由。你能够在你本身的代码里经过使用__builtin_trap()函数来触发这个异常。若是没有debugger存在,则线程会被终止并生成一个crash report。
底层库(例如libdispatch)会在遇到fatal错误的时候陷入这个困局。关于错误的相关信息会在crash report的章节或者是设备的的打印信息里找到。
Swift代码会在运行时的时候遇到下述问题时抛出这种异常:
· 一个non-optional的类型被赋予一个nil值
· 一个失败的强制转换
遇到这种错误,查下堆栈信息并想清楚是在哪里遇到了未知状况(unexpected condition)。额外信息也可能会在设备的控制台的日志里出现。你应当尽可能修改你的代码,去优雅的处理这种运行时错误。例如,处理一个optional的值,经过可选绑定(Optional binding)而不是强制解包来得到其值。
孟嵩:可选绑定,就是相似以下语句的使用
if let actualValue = maybeHasValue(){
foo(actualValue)
}
Illegal Instruction [EXC_BAD_INSTRUCTION // SIGILL]
当尝试去执行一个非法或者未定义的指令时会触发该异常。有多是由于线程在一个配置错误的函数指针的误导下尝试jump到一个无效地址。
在Intel处理器上,ud2操做码会致使一个EXC_BAD_INSTRUCTIONY异常,可是这个一般用来作调试用途。在Intel处理器上,Swift会在运行时碰到未知状况时被中止。 详情参考Trace Trap。
这个异常是因为其它进程拥有高优先级且能够管理本进程(所以被高优先级进程Kill掉)所致使。SIGQUIT不表明进程发生Crash了,可是它确实反映了某种不合理的行为。
iOS中,若是占用了太长时间,键盘扩展程序会随着宿主app被干掉。所以,这种状况的异常下不太可能会在Crash report中出现合理可读的异常代码。大几率是由于一些其它代码在启动时占用了太长时间可是在总时间限制前(看门狗的时间限制,见上文中的表格)成功结束了,可是执行逻辑在extension退出的时候被错误的执行了。你应该运行Profile,仔细分析一下extension的各部分消耗时间,把耗时较多的逻辑放到background或者推迟(推迟到extension加载完毕)。
进程收到系统指令被干掉。请自行查看Termination Reason来定位线程被干掉的缘由。
Termination Reason字段会包含一个命名空间和代码。如下代码只针对watchOS:
· 代码0xc51bad01表示watchOS在后台任务占用了过多的cpu时间而致使watch app被干掉。想要解决这个问题,优化后台任务,提升CPU执行效率,或者减小后台的任务运行数量。
· 代码0xc51bad02表示在后台的规定时间内没有完成指定的后台任务而致使watch app被干掉。想要解决这个问题,须要当app在后台运行时减小app的处理任务。
· 代码0xc51bad03表示watch app没有在规定时间内完成后台任务,且系统一直很是忙以致于app没法获取足够的CPU时间来完成后台任务。虽然一个app能够经过减小自身在后台的运行任务来避免这个问题,可是0xc51bad03这个错误把矛头指向了太高的系统负载,而非app自己有什么问题。
进程违规访问了一个被保护的资源。系统库会把特定的文件描述符标记为被被保护,所以任何对这些文件描述符的常规操做都会抛出EXC_GUARD异常(nimo: 当系统想操做这些文件描述符时,它们会用特殊的被受权过的私有API)。因此遇到诸如私自关闭掉系统打开的文件描述符之类的操做时您能够快速察觉。例如,若是一个app关闭掉了曾经支持Core Data 存储的SQLite文件的文件描述符,你会发现Core Data过一下子神秘crash。guard exception会在不久以后注意到而且让他们更容易被debug。
更新版本的iOS crash report会在Exception Subtype和Exception Message字段里包含关于EXC_GUARD异常的可读详细信息。在macOS或者是更老版本的iOS的crash report中,这条信息会被加密成第一个Exception Code并以位信息进行呈现,它能够被这么解读:
· [63:61] - Guard Type:被保护的资源的类型。0x2值表示资源是一个文件描述符。
· [60:32] - Flavor:在何种状况之下出现的问题。
若是第一个(1<<0)bit被设值,则进程尝试在一个被保护的文件描述符上调用close()
若是第二个(1<<1)bit被设值,则进程尝试在被保护的文件描述符上用F_DUPFD 或 F_DUPFD_CLOEXEC调用dup(), dup2(), 或 fcntl()命令。
若是第三个(1<<2)bit被设值,则进程尝试经过socket发送一个被保护的文件描述符。
若是第五个(1<<4)bit被设值,则进程尝试写一个被保护的文件描述符。
· [31:0] - File Descriptor:进程尝试修改被保护的文件描述符。
进程的资源超过限定阈值。这条推送是OS发出的,表示进程占有了太多的资源。准确的资源列在了Exception Subtype的字段里。若是Exception Note字段包含了NON-FATAL CONDITION(非严重错误),则即使是生成了crash report,进程也不会被kill掉。
· 若是EXCEPTION SUBTYPE里出现MEMORY则暗示了进程占用已经超过系统限制。若是以后出现因为系统占用过多进程被Kill,可能和这有关。
· 若是EXCEPTION SUBTYPE里出现WAKEUP则暗示线程每秒被进程唤醒太屡次了,进而致使CPU被频繁唤醒而且形成电量损耗。
一般,这种事发生在进程间通讯(经过peformSelector:onThread:或者dispatch_async),并且会远比预想的发生的更频繁。由于发生这种异常的通讯被触发的如此频繁,因此不少后台进程会出现彼此高度雷同的堆栈信息——偏偏暗示了它们是从哪儿来的。
有些crash report可能会出现无名的Exception Type,这时候在这个字段上会出现纯16进制值(例如00000020)。若是你收到这样的crash report,直接去Exception Code查看更多信息。
· 若是Exception Code是0xbaaaaaad则说明此条logs是系统堆栈快照,并不是crash report。能够经过同时按(手机)侧边按钮和音量键来记录堆栈快照。一般状况下,这些logs是用户无心中生成的,并不是表示错误。
· 若是Exception Code是0xbad22222表示一个VoIP应用由于频繁暂停被iOS系统终止掉。
· 若是Exception Code是0x8badf00d(读起来像badfood)则说明一个应用由于触发了看门狗机制被iOS系统终止掉,有多是应用花了太长时间启动,终止,或者是响应系统事件。一种常见缘由是在主线程上作网络同步逻辑。不论Thread0上(也就是主线程)想作什么(重要的事),都应该转移到后台线程,或者换一种方式触发,这样它才不会阻塞主线程。
· 若是Exception Code是0xc00010ff则说明app由于环境过热(的事件)被iOS系统干掉了。这个也许是和发生crash的特定设备有关,或者是和它所在的环境有关。若是想知道更多高效运行app的tips,请参考WWDC的文章: iOS Performance and Power Optimization with Instruments。
· 若是Exception Code是0xdead10cc(读起来像deadlock)则说明一个应用被系统终止掉,缘由是在应用挂起时拿到了文件锁或者sqlite数据库所长期不释放直到被冻结。若是你的app在挂起时拿到了文件锁或者sqlite数据库锁,它必须请求额外的后台执行时间(request additional background execution time )并在被挂起前完成解锁操做。
· 若是Exception Code是0x2bad45ec则说明app由于违规操做(安全违规)被iOS系统终止。终止描述会写:“进程被查到在安全模式进行非安全操做”,暗示app尝试在禁止屏幕绘制的时候绘制屏幕,例如当屏幕锁定时。用户可能会忽略这种异常,尤为当屏幕是关闭的或者当这种终止发生时正好锁屏。
Note:经过App Switcher(就是双击home键出现的那个界面)并不会生成crash report。一旦app进入挂起状态,被iOS在任什么时候间终止掉都是合理的,所以这时候不会生成crash report。
本章节包含终止相关的额外诊断信息,包括:
· 应用的具体信息:在进程被终止前捕捉到的框架错误信息
· 内核信息:关于代码签名问题的细节
· Dyld (动态连接库)错误信息:被动态连接器提交的错误信息
从macOS Sierra, iOS 10, watchOS 3, 和 tvOS 10开始,大部分这种信息都在Exception Information 的Termination Reason字段下了。
你应当阅读本章节来更好的明白当进程被终止的时候发生了什么。
表7:一段由于找不到连接库而致使进程被终止的crash report的摘录
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.
表8:一段由于没能快速加载初始view controller而致使进程被终止的crash report的摘录
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
列表9:一个彻底符号化的crash report的堆栈部分摘录
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
...
第一行列出了当前的线程号,以及当前的执行队列的id。其他各行列出来每个堆栈中堆栈片断信息,从左到右分别是:
· 堆栈片断号。堆栈的展现顺序会和调用顺序一致,片断0是在程序被终止时执行的函数。片断1是调用片断0的函数,以此类推。
· 在堆栈片断中驻留的执行函数的名称
· 片断0表明机器指令在被终止的生活所在的地址。其它片断表示若是片断0执行完成以后下一个执行的片断地址
· 在一个符号化的crash report中,表明在堆栈片断中的函数名称
Objective-C中的异常一般用来代表在运行时发生的代码错误,例如越界访问数组,或者更改immutable的对象,没有实现protocol中必须实现的方法,或者给接收者没法识别的对象发送信息。
Note:给以前已经释放的对象发送消息会引起NSInvalidArgumentException异常进而crash,而非内存访问违规。这会在新的变量正好占据了以前释放变量所在内存时。若是你的app由于NSInvalidArgumentException发生crash(在堆栈信息中查看[NSObject(NSObject) doesNotRecognizeSelector:]),考虑经过 Zombies instrument 来profiling你的应用,来排除刚才提到的内存管理问题。
若是异常没有被捕捉到,他会被一个叫uncaught exception方法所拦截。默认的uncaught exception的日志会显示到设备的控制台,以后会终止进程。异常堆栈信息会在生成的crash report的上一个异常堆栈(Last Exception Backtrace)下,就像列表10所写。异常消息会被crash report忽略。若是你收到了一个带有上一个异常堆栈(Last Exception Backtrace)的crash report,你应当去获取原始设备并获取其控制台日志信息,来更好的了解发生crash的缘由。
List10:发生了上一个异常堆栈(Last Exception Backtrace)的未符号化crash report摘录
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)
一个只包含16进制信息的有Last Exception Backtrace信息的crash日志必须被符号化,以获取有价值的堆栈信息,就像列表11所写。
列表11:一个包含Last Exception Backtrace信息的符号化的crash report。这个异常出如今加载app的storyboard时,须要响应的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)
若是你发现本应该被捕捉的异常并无被捕捉到,请肯定您没有在building应用或者library时添加了-no_compact_unwind标签。
64位IOS用了zero-cost的异常实现机制。在zero-cost系统里,每个函数都有一个额外的数据,它会描述若是一个异常在跨函数范围内实现,该如何展开相应的堆栈信息。若是一个异常发生在多个堆栈可是没有可展开的数据,那么异常处理函数天然没法跟踪并记录。也许在堆栈很上层的地方有异常处理函数,可是若是那里没有一个片断的可展开信息,没办法从发生异常的地方到那里。指定了-no_compact_unwind标签代表你那些代码没有可展开信息,因此你不能跨越函数抛出异常(也就是说没法经过别的函数捕捉当前函数的异常)。
这章列出了crash线程的线程状态。这里列出了注册过的值。在你读一个crash report的时候,了解线程状态并不是必须,可是若是你想更好地了解crash的细节,这也许会起一些帮助。
列表12:ARM64设备的crash report的一段Thread State摘录
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
这一章列出了在进程被终止时加载在进程中的二进制文件(binary images)。
列表13:一段crash report的完整二进制文件摘录
Binary Images:
0x100060000 - 0x100073fff TheElements arm64 <2defdbea0c873a52afa458cf14cd169e> /var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
...
每一行都包含了一个二进制文件的如下细节信息:
· 在进程内的二进制文件的地址空间
· 一段二进制的名称或者bundle id(仅针对macOS)。一个MacOS的crash report,若是二进制时OS的一部分,会在前面加上a。
· (仅针对macOS)二进制的短版本(short version)和bundle版本,经过破折号来分割。
· (仅针对iOS)二进制文件的架构名。一个二进制可能包含多个分片,每个架构它都支持。其中只有一个能够被加载到进程中。
· 一个能够惟一标示二进制文件的id,即UUID。这个值会随每一次构建而发生变化,而且它会用来定位须要符号化时的dSYM文件。
· 磁盘上二进制文件的path。
当系统检测到内存不足时,iOS系统里的虚拟内存系统会协同各应用来作内存释放。每一个运行着的应用都会接收到系统发来低内存推送(Low-memory notification),要求释放内存空间,从而达到减小总体内存消耗的目的。若是内存压力依然存在,系统可能会终止后台进程以减轻内存压力。若是(总体)内存释放够了,你的应用将能够继续运行;否则,你的应用会被iOS终止,由于可供你的应用运行的内存不够,这时候会生成一个低内存 report(Low-Memory Report)并存储在你的设备中。
低内存 report的格式和其它crash report略有不一样,它没有应用的堆栈信息。一个低内存 report的Header会和crash report的header有些相似。紧接着Header的时各个字段的系统级别的内存统计信息。记录下页大小(Page Size)字段。每个进程的内存占用大小是根据内存的页的数量来 report的。
一个低内存 report最重要的部分是进程表格。这个表格列出了全部的运行进程,包括系统在生成低内存 report时的守护进程。若是一个进程被”遗弃”了,会在[缘由]一列附上具体的缘由。一个进程可能被遗弃的缘由有:
· [per-process-limit]:进程占用超过了它的最大内存值。每个进程在常驻内存上的限制是早已经由系统为每一个应用分配好了的。超过这个限制会致使进程被系统干掉。
注意:扩展程序(nimo: Extension app, 例如输入法等)的最大内存值更少。一些技术,例如地图视图和SpriteKit,占用很是多的基础内存,所以不适合用在扩展程序里。
·
· [vm-pageshortage]/[vm-thrashing]/[vm]:因为系统内存压力被干掉。
· [vnode-limit]: 打开太多文件了。
注意:系统会尽可能避免在vnodes已经枯竭的时候干掉高频app。所以你的应用若是在后台,即使并无占用什么vnode,而有可能被杀掉。
· [highwater]:一个系统守护进程超过过了它的内存占用高水位(就是已经很危险了)。
· [jettisoned]:进程由于其它不可描述的缘由被杀掉。
若是你没有在你的应用或者扩展程序里看到缘由,那crash的缘由就不是低内存压力。仔细查看一下.crash文件(在以前章节里有写)。
当你发现一个低内存crash,与其去担忧哪一部分的代码出现问题,还不如仔细审视一下本身的内存使用习惯和针对低内存告警(low-memory warning)的处理措施。Locating Memory Issues in Your App 列出了如何使用Leaks Instrument工具来检查内存泄漏,和如何使用Allocations Instrument的Mark Heap 功能来避免内存浪费。 Memory Usage Performance Guidelines 讨论了如何处理接受到低内存告警的问题,以及如何高效使用内存。固然,也推荐你去看下2010年的WWDC中的 Advanced Memory Analysis with Instruments 那一章节。
重要:Leaks和Allocation工具不能检测全部的内存使用状况。你须要和VM Tracker工具一块儿运行(包含在Allocation工具里)来查看你的内存运行。默认VM Tracker是不可用的。若是想经过VM Tracker来profile你的应用,点击instrument工具,选中”Automatic Snapshotting”标签或者手动点击”Snapshot Now”按钮。
若是想查看如何使用Zombies模板工具来修复内存释放的crash,能够查看Eradicating Zombies with the Zombies Trace Template 。
若是想查看应用归档的信息,请参考 App Distribution Guide 。
若是想了解关于crash logs的解读,请参考Understanding Crash Reports on iPhone OS WWDC 2010 Session 。
这次苹果新发布的6.1英寸iPhone XR、5.8英寸iPhone XS、6.5英寸iPhone XS Max,WeTest将会第一时间收入机房,关注WeTest官方报道,获取最新机型上线时间。