iOS Crash三部曲~之三Crash分析
1 iOS Crash Report
app发生crash时会产生crash report,这对我们定位crash的原因非常有帮助。 crash report会描述app在何种情况之下被系统终止运行,一般情况下描述会包括完整的线程调用堆栈,这对app的调试(和问题的定位)是非常有帮助的。所以我们应当仔细研读这些crash report,去了解你的app究竟发生的是哪种crash,并尝试修复它们。
Crash Report,尤其是堆栈信息,在被符号化之前是不可读的。所谓符号化就是把内存地址用可读的函数名和行数来替换。如果你不是从设备直接获取的crash日志,而是通过Xcode的Device Window(即通过视图操作而非手动命令行),它们会在几秒之后自动被符号化。当然你也可以把.crash文件加入到Xcode的Device Window并自行将它符号化。
Low Memory Report与其它crash report不同,它没有堆栈信息。当由于低内存而发生crash时,这里我们必须反思我们的内存使用模式和针对低内存警告的应对方法。后面会提供给几个内存管理的参考实现。
针对crash分析,我们主要搞清楚三件事就可以。
-
符号化:就是把不可读的文档转成可读
-
看懂:意思就是弄清楚文档中每一个部分表达的是什么
-
解析:意思就是能从文档中定位出问题,拿到解决问题的关键有价值的信息
2 获取Crash Report和Low Memory Report
获取崩溃报告,有以下四种方法,其中自己收集的方式,可以参考《iOS Crash三部曲~之一Crash分类》一章有介绍。
1、自己收集(自建一套打点平台)
2、第三方收集(bugly、听云、友盟、TalkingData 等等)
3、Xcode工具
4、苹果官方提供的crash log崩溃收集服务
2.1 获取Report --Xcode工具
打开Xcode—Window—Devicesandsimulators, 选中设备, 点击“View Device log” 查看
image image上图是一个《TestCrash》app的一个crash log,如果我们电脑之前Archiv的包,也就是本地Archives下面有对应的打包信息,这里在看log的时候,点击之后,几秒之后,会自动给符号化。
image2.2 获取Report --苹果官方
这是获取用户的crash log,这个是需要用户配合的,因为需要用户在手机中 设置->隐私-->分析 打开共享 , 然后与应用开发者共享开关也打开。
然后在Xcode中Window->Organizer->Crashes对应的app,就是当前app最新一版本的crash log ,并且是解析过的,可以根据crash栈 等相关信息 ,尤其是程序代码级别的 有超链接,一键可以直接跳转到程序崩溃的相关代码,这样更容易定位bug出处。
image3 符号化一篇Crash report
符号化指的是一种手段,这种手段指的是把堆栈信息(二进制信息)解释成源码里的方法名或者函数名,也就是所谓符号。只有符号化成功后,crash report才能帮助开发者定位问题。 Low Memory Report不需要被符号化(因为没有堆栈信息)。
注意:在MacOS平台上产生的crash report在生成的时候一般都会被完全符号化过或者半符号化过。这里的符号化针对的是从iOS、watchOS乃至tvOS中提取出来的crash report。整体处理流程上,macOS的carshreport比较类似。
image1、编译器在把源代码转换成机器码的同时,也会生成一份对应的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 review或者客户的crash report,你需要保留分发出去的那些构建过的archive文件。
3、如果是通过App Store分发app或者是Test Flight分发的beta版本的app,你将在上传archive到ITC(iTunes Connect)时看见一个“是否将dSYM一起上传”的选项。在上传对话框中,请勾选”在app中包含app符号表”。上传你的dYSM文件对于从TestFlight用户和客户以及愿意分享诊断信息的客户那边接收crash report是很有必要的。
注意:接收自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中进行查看。
4 什么是UUID
每一个可执行程序都有一个build UUID来唯一标识。Crash日志包含发生crash的这个应用的 build UUID以及crash发生的时候,应用加载的所有库文件的[build UUID]。
dSYM文件是指具有调试信息的目标文件,文件名通常为:xxx.app.dSYM。
1、在终端查看.crash文件的UUID:
grep “appNamearm” *crash或
grep --after-context=2 "Binary Images:" *crash
image
注意:0x1028d8000 是模块的加载地址,后面用atos的时候会用到。
2、查看.app的UUID
xcrun dwarfdump --uuid TestCrash.app/TestCrash
TestCrash为需要查看的.app的名字
image这个app有2个UUID,对比上面crash文件和app文件的UUID,发现它们是匹配的:13A2B0F6-4E83-3C0F-834A-B3027C4B6164。
3、查看.dSYM的UUID
dwarfdump --uuid TestCrash.app.dSYM
TestCrash为需要查看的.app的名字
image5 如何判断Crash report是否已经符号化
一个crash report有可能未符号化,完全符号化,也有可能部分符号化。未符号化的crash report不会在堆栈信息中包含方法名或者函数名,用处有限。相反,你会在加载好的binary信息中发现可执行的16进制地址信息。在完全符号化的crash report里,堆栈中的每一行16进制地址信息都会被替换成对应的符号。
在部分符号化的crash report中,只有一部分堆栈信息被替换成相应的符号信息。
完全符号化的crash report,才能够获得crash report里最有价值的信息。一个部分符号化的crash report也许包含了可以理解crash的信息,这取决于crash的类型和哪一部分被成功符号化了。
image6 符号化
6.1 手动符号化-方法1
第一种方法就是,使用Xcode完全自动化实现符号化, 我们需要将下面所列的3个文件放到同一个目录下。
-
1、crash报告(.crash文件)
-
2、符号文件(.dsymb文件),dSYM文件是指具有调试信息的目标文件,文件名通常为:xxx.app.dSYM
-
3、应用程序文件 (appName.app文件)
打开Xcode--Window Devices and simulators,然后点击Devices,点击viewdevicelogs,然后把.crash文件拖到Device Logs或者选择下面的import导入.crash文件,等待几秒钟即可符号化完成。
image6.2 手动符号化-方法2
有时候Xcode不能够很好的符号化crash文件。这里介绍如何通过symbolicatecrash来手动符号化crash log。 也需要将.crash、.dsymb、.app 三个文件放在同一个目录下。
第1步:首先要找到symbolicatecrash路径,命令行执行以下命令。
find /Applications/Xcode.app-name symbolicatecrash-type f
将会返回所有的xcode自带的 symbolicatecrash工具位置, 如下图所示。
image第2步:然后cd到三个文件夹目录下,执行以下命令。
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrashTestCrash.crashTestCrash.app> TestCrash.log
最终生成.log文件,也就是符号化之后的崩溃报告。
如果遇到DEVELOPER_DIR" is not defined at…… 报错,如下图所示。
image执行以下命令之后,在重新按照第2步骤生成.log文件。
export DEVELOPER_DIR=“/Applications/XCode.app/Contents/Developer
6.3 手动符号化-方法3
使用命令行工具atos,atos命令可以把地址里的数字替换成等价的符号。如果调试符号信息是完备的,则atos的输出信息将会包含文件名和对应的资源行数。atos命令可以被用来单独符号化那些未符号化或者部分符号化过的crash report(中的堆栈信息里的地址)。
第1步:找到错误位置,找到想要符号化的那一行,记下第二列的binary信息名,以及第三列的地址。例如找到报错的语句,记录appName那一行的第一个地址:0x00000001028e14cc 就是一个错误地址。
image第2步:查找crash模块的加载地址,执行以下命令。
grep "TestCrasharm" *crash
image
这里的 0x1028d8000 就是加载地址, arm64 architecture 的值
第3步:用atos命令来符号化某个特定的模块加载地址,执行以下命令。
xcrunatos-o TestCrash.app.dSYM/Contents/Resources/DWARF/TestCrash-l 0x1028d8000 -arch arm64
第4步:输入完上面命令后,会进入到一个带输入状态,此时输入第1步得到的地址:0x00000001028e14cc,即可得到结果。
image7 分析
每一篇crash report都有一个header。
image-
Incident Identifier: 一个crash report的唯一ID。两个report不会使用同一个Incident Identifier。
-
CrashReporterKey: 一个匿名的设备相关ID。同一个设备的两篇crash report会有相同的CrashReporterKey。
-
Beta Identifier:一个整合了发生crash app的设备和供应商信息的ID。来自同一个供应商和设备的两篇report会包含相同的ID值。这个字段只有当app通过TestFlight分发的时候出现,并且出现在应该出现Crash Reporter Key Field的地方。
-
Process:发生Crash时的进程名。这个和app信息属性列表里的CFBundleExecutableKey中的值可以匹配上。
-
Version:发生crash的版本号。这个值可以关联到发生crash的app的CFBundleVersion和CFBundleVersionString上。
-
Code Type:发生crash的上下文所在架构环境。有ARM-64,ARM,X86-64和X86.
-
Role:在发生crash时进程的的task_role。
-
OS Version: OS version,包含发生crash时的所属app的编译码。
-
‘Exception Type’:说明崩溃产生的原因,具体的崩溃具体分析
主要就需要注意4个地方:
1、Process
2、Version
3、OS Version
4、Exception Type
image通过Process名字和版本,一眼就能看出来这个是我们当前app的崩溃日志,然后我去分析这行代码周围的代码很快就能找到问题所在了。Mach异常类型和相应的能提供crash的蛛丝马迹的一些字段信息。当然,不是所有字段都会出现在每一篇crash report里。
-
Exception Type’:这一部分字段解析:
-
Exception Codes: 和异常是有关的处理器指定信息,这些信息会被编码成一个或者多个64位二进制数字。一般来说,这个字段不应该存在,因为crash report生成时会把Exception code:转化成可读的信息并在其它字段进行体现。
-
Exception Subtype:可读的exception code的名称。
-
Exception Message:从exception code中解析出来的附加的可读信息。
-
Exception Note:不特指某一种异常的额外信息。如果这个字段包含”SIMULATED”(不是Crash),则进程并没有发生crash,而是在系统层面被kill掉了,比如看门狗机制。
7.1 Mach异常-1.EXC_BAD_ACCESS
访问一块坏内存(Bad Memory Access)是指进程试图去访问无效的内存空间,或者尝试访问的方法是不被允许的(例如给只读的内存空间做写操作)。在Exception Subtype字段中如果出现kern_return_t的话,说明内存地址空间被不正确的访问了。
几点调试Bad Memory Access导致崩溃的建议:
-
假如objc_msgSend或者objc_release出现在crash的线程的附近,这个线程可能尝试给一个释放的对象发消息。这时profile应用使用Zombies instrument来更好的了解这个崩溃发生的原因。
-
假如gpus_ReturnNotPermittedKillClient出现在crash的线程附近,线程被终结因为它尝试用OpenGL ES或者Metal执行渲染当程序处于后台时。查看QA1766: How to fix OpenGL ES application crashes when moving to the background
Address Sanitizer(ASan)是一个快速的内存错误检测工具。它非常快,只拖慢程序两倍左右(比起Valgrind快多了)。它包括一个编译器instrumentation模块和一个提供malloc()/free()替代项的运行时库。Address Sanitizer是基于LLVM的适用于C(包括Objective-C)和Swift的用于发现内存使用问题的工具。
Address Sanitizer的原理: 启用Address Sanitizer后,会在APP中增加libclang_rt.asan_ios_dynamic.dylib,它将在运行时加载。Address Sanitizer替换了malloc和free的实现。当调用malloc函数时,它将分配指定大小的内存A,并将内存A周围的区域标记为”off-limits“。当free方法被调用时,内存A也被标记为”off-limits“,同时内存A被添加到隔离队列,这个操作将导致内存A无法再被重新malloc使用。
**7.2 Mach异常- 2.EXC_CRASH **
异常退出(Abnormal Exit)是指程序异常退出,是最常见导致这类异常崩溃的原因是捕获到Objective-C/C++异常和调用了abort()函数。
App Extensions将被终结发生这种类型的异常,假如他们初始化花费太多的时间(watchdog终结)。
假如一个extension由于载入时间太长被终结,产生崩溃报告的Exception Subtype会写LAUNCH_HANG。所以在初始化构造方法中,或者+load方法,初始化太多,会增加时间,造成看门狗机制。我们需要注意,把一些非必要的操作尽可能延迟去做。
**7.3 Mach异常- 3.EXC_BREAKPOINT **
追踪受限(Trace Trap)和异常退出类似,这种异常的目的是给一个追加的调试器,让它有机会来打断在一个当它执行时候指定的点的进程。我们可以使用__builtin_trap()函数在代码中来触发这个异常。假如没有调试器追加的话,线程将被终结并且产生一个崩溃报告。
低等级的库(例如libdispatch)将受限这个进程一旦遇到一个重大的错误。关于错误的额外信息可以在崩溃报告找到,或者在设备的控制台。
Swift代码会在运行时的时候遇到下述问题时抛出这种异常:
-
非可选类型带有一个nil值。
-
错误的强制类型转换。
**7.4 Mach异常- 4.EXC_BAD_INSTRUCTION **
非法指令(Illegal Instruction)是指进程尝试执行一个非法或者未定义的指令时会触发该异常。进程可能通过一个配置错误的函数指针尝试跳进到一个无效的地址。
在Intel处理器中,ud2操作码导致一个EXC_BAD_INSTRUCTION异常,但是它通常被用来困住进程达到调试的目的。Swift代码在Intel处理器中,假如在runtime碰到未知情况,会直接终结。
7.5 Mach异常- 5.EXC_GUARD
被保护的资源遭到侵害(Guarded Resource Violation)是指进程违规访问一个被保护的资源。系统库会把特定的文件描述符标记为被被保护,所有任何对这些文件常规的操作都将触发一个EXC_GUARD异常(ps:当它想操作在这些文件描述器上,系统可以使用特殊的guarded标记的私有APIs)。
崩溃报告中包含了可读的详细信息,在Exception Subtype和Exception Message字段中。在来自macOS或者老版本的iOS的崩溃报告中,这些信息被编码到第一个Exception Code可以一个分解成如下的位段:
-
Guard Type:被保护的资源类型。0x2代表一个文件描述器资源。
-
Flavor:侵害被处罚时的条件
假如第一个(1 << 0)位被设置,进程尝试在被保护的文件描述上执行close()操作。
假如第二个(1 << 1)位被设置,进程尝试在被保护的文件描述执行dup(),dup2(),fcntl、或F_DUPFD、F_DUPFD_CLOEXEC命令。
假如第三个(1 << 2)位被设置,进程尝试通过一个socket发送给一个受保护的文件描述器。
假如第三个(1 << 3)位被设置,进程尝试写入到一个受保护的文件描述器。
- File Descriptor:进程尝试修改的受保护的文件描述器。
7.6 Mach异常- 6.EXC_RESOURCE
资源限制(Resource Limit)是指进程消耗的资源超出了限制阈值。这是一个来自操作系统通知,告诉进程正在使用的资源过多。准确的资源列在Exception Subtype字段中。假如Exception Note字段包含NON-FATAL CONDITION(非严重错误) ,进程不会被终结,但会产生了一个崩溃报告。
-
Exception Subtype出现MEMORY表明进程已经越过系统应用的内存限制。这可能是一个终结的先兆由于超额的使用内存。
-
Exception Subtype 出现WAKEUPS表明在进程中的线程每秒被唤醒太多次,这强制CPU非常频繁的唤醒消耗电池寿命。
通常这个发生在线程与线程的通信(通常在使用peformSelector:onThread:或dispatch_async),会比预想的发生更加频率。这个通常很多个后台线程有着相似Backtraces– 这也正是表明那些地方发生过通信。
7.7 Mach异常- 7.EXC_ARITHMETIC
当要除零时,应用会收到EXC_ARITHMETIC信号。这个错误很容易处理。
7.8 Mach异常-8.其他异常类型
一些崩溃报告可能会含有一个未命名的Exception Type,它将以一个16进制的值(例如:c00010ff)的形式打印。假如设备收到了一个这样的崩溃报告,直接查看Exception Codes字段寻找更多的信息。
以下几个常见的:Exception Code:
-
0xbaaaaaad:此种类型的log意味着该Crash log并非一个真正的Crash,它仅仅只是包含了整个系统某一时刻的运行状态。通常可以通过同时按Home键和音量键,手机死机的时候,会如此操作。
-
0xbad22222:当VOIP程序在后台太过频繁的激活时,系统可能会终止此类程序
-
0x8badf00d:程序被watch dog终止。应用花费太长时间启动、响应系统事件。通常导致这个问题是做了在主线程执行了同步的请求。无论什么操作在Thread 0都需要移动到后台线程,或者异步处理,以免它阻塞主线程。
-
0xc00010ff:程序执行大量耗费CPU和GPU的运算,导致设备过热,触发系统过热保护被系统终止。为了使应用更高效运行可以查看WWDC session iOS Performance and Power Optimization with Instruments。
-
0xdead10cc:程序退到后台时还占用系统资源(如通信录数据库) ,被系统终止。
-
0xdeadfa11:表明应用被用户强制退出。强制退出发生在当用户第一次按下开关机按钮直到"滑动来关机"出现,系统强制退出任何正在运行的任务。
全部Exception Code:https://www.jianshu.com/p/26125cbb116d
这些地址是怎么被定义的呢??
image8 理解Low Memory Reports
当内存不足发生时,(Low-memory notifications)被发送到每一个正在运行的app或者进程,要求释放内存,从而减少内存压力。如果内存还是不足系统会终止background processes 来缓解内存压力。如果能释放出足够的内存来,那app会继续运行。否则app会被iOS强制终止,因为没有足够的内存让app继续执行下去,这时就会有low memory report生成。
low memory report与其它的crash report不同,它没有backtraces。它的头和一般的crash report的Header相似。
Header之后就列举了系统的内存统计信息。其中Page Size字段最值得关注。
low memory report中最重要的部分当属进程表了。进程表列举了low memory report生成时所有正在运行的进程,包括系统守护进程。如果一个进程被jettisoned( ”遗弃” ),原因会写在[reason]列。
一个进程被jettisoned可能因为下列原因:
-
[vm-pageshortage]/[vm-thrashing]/[vm]:由于系统内存压力被杀掉。
当你看到一个law memory crash,应该考虑检查消耗内存的代码和对low memory通知的响应。 Memory Usage Performance Guideline
Advanced Memory Analysis with Instruments
每一个进程在常驻内存上的限制是早已经由系统为每个应用分配好了的。超过这个限制会导致进程被系统干掉。使用Leaks Instrument工具来检查内存泄漏,和如何使用Allocations Instrument的Mark Heap 功能来避免内存浪费。
相关文章: