了解和分析应用崩溃报告

2018-01-18  本文已影响1420人  alvin_wang

当一个应用崩溃时,会产生一个崩溃报告。这个报告对分析崩溃问题是非常有用的。这篇文章主要讲述了如何符号化、了解和分析应用报告。

1. 简介

当一个应用崩溃时,会随之产生一个崩溃报告存储在设备上。崩溃报告描述了当时的崩溃情况,在绝大多数时候包含每个线程的堆栈回溯。你可以通过分析这些报告来了解应用的崩溃并且修复它们。

在分析一个报告之前,你需要先符号化它。符号化就是把原始的地址信息转换为刻度的函数名和对应的行数。如果你通过Xcode的devices窗口获得一个设备的崩溃日志,它会自动被符号化。

低内存报告与其他报告是不同的,它没有堆栈回溯信息。当由于低内存引发崩溃后,你必须研究一下你的内存使用方式和对内存警告的处理。

2. 符号化崩溃报告

符号化是一种把堆栈地址转换为源码汇总函数名的过程。一个没有符号化的崩溃报告是无法分析出崩溃如何发生的。

注意:低内存报告不需要符号化。

tn2151_crash_flow.png

1.随着编译器把源码转换为机器码,它同时产生了一堆debug符号,这写符号记录着源码和机器码之间的对应关系。根据Build Settings中的Debug Infomartion Format,这些符号会被存入一个二进制文件中或者DSYM中。默认的选项是存入DSYM文件中,这样会使文件的体积更小。

Debug符号文件通过UUID来和应用的二进制文件建立联系。每次build一个应用,都会产生一个新的UUID。即使源码一模一样,编译器设置也一样,build之后的UUID都是不同的。

2.当你archive一个应用准备发布时,Xcode会把应用的二进制文件和DYSM文件放在同一个目录下。

注意:为了能够符号化来自tester,App review和用户的崩溃报告,你必须保留每次发布的archive。

3.如果你通过App Store发布应用,或者通过Test Flight发布测试版本应用,你都将会收到一个选择是否上传DYSM文件的选择。对于TestFlight用户上传的崩溃报告,必须要上传DSYM文件。

注意:来自App review的崩溃报告不会被符号化,即使你上传了DSYM文件。你需要使用Xcode来符号化崩溃报告。

4.当你的应用崩溃了,一个没有符号化的崩溃报告会被存储在设备上。

5.取回用户的崩溃报告,一是可以通过Xcode直接获得设备的报告,二是用户的手机设置了允许上传诊断数据。

6.通过设备取回的崩溃报告是没有符号化的,需要通过Xcode来使用DSYM文件符号化它。

7.如果用户开启了与apple共享数据的选项,或者用户通过TestFlight安装了beta版应用,崩溃报告会上传到App Store。

8.App Store会符号化收到的崩溃报告。

9.可以通过Xcode的Crashes organizer来读取符号化的崩溃报告。

2.1 Bitcode

BitCode是编译后的程序的一种中间呈现代码。当你在bitcode选项打开时archive一个应用,编译器会产生包含bitcode的二进制,而不是机器码。一旦这个二进制被上传到App Store,bitcode会被编译成机器码。这样的话,如果编译器性能提高了就不需要再次编译二进制,而是通过App Store直接再次编译bitcode。


tn2151_bitcode_overview.png

由于最终的编译时在App Store完成的,因此你的Mac电脑上存储的DSYM文件不能够符号化来自App review或者用户的崩溃报告。App Store在把bitcode转换为机器码时会产生一个DYSM文件,你可以通过Xcode或者iTunes Connect网站下载。你必须使用这个DYSM文件来符号化上述的崩溃报告。而通过崩溃报告服务上传的崩溃报告会被App Store自动符号化。

注意:原始提交的二进制映像的UUID和被App Store编译后的二进制映像的UUID是不同的。

通过Xcode下载DSYM文件
  1. 在Archives organizer中,选择提交到App Store的archive。
  2. 点击下载DSYMs按钮。
通过iTunes Connect网站下载DSYM
  1. 打开App详情页面。
  2. 点击Activity。
  3. 从一系列builds中选择一个版本。
  4. 点击下载DSYM链接。
转换隐藏符号名到原始符号名

当你上传带有bitcode的应用到App Store去时,你可以不勾选弹窗中的"Upload your app's symbols to receive symbolicated reports from Apple" 选项。如果你不勾选,Xcode会混淆.dSYM文件,再上传到iTunes Connect上。Xcode会在原始符号和隐藏符号之间建立一个mapping,然后把这个mapping存入.bcsymbolmap文件中,这个文件会包含在archive中。

在符号化崩溃报告时,你需要解密从iTunes Connect下载的.dSYM文件。如果你是从Xcode下载的.dSYM文件,Xcode会自动解密。从iTunes Connect下载的.dSYM文件可以通过如下命令行解密:

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
2.2 是否符号化一个崩溃报告

一个崩溃报告可能是没被符号化的,或者全部符号化的。或者部分符号化的。没有符号化的崩溃报告不会在回溯信息中包含方法或者函数名。你得到的是16进制的地址。完全符号化的崩溃报告,每一行16进制地址都被转换为响应的符号。部分符号化的崩溃报告,只有部分地址被符号化。

对于分析崩溃报告,明显是需要一个完全符号化的报告。


tn2151_symbolication_levels.png
2.3 通过Xcode来符号化崩溃报告

Xcode会尝试自动符号化一个崩溃报告。你需要做的只是把崩溃报告放入Xcode Organizer中。

注意:Xcode不会解析不以.crash为后缀的文件。如果你收到一个不正确后缀的报告,把后缀为.crash

  1. 连接iOS设备到Mac
  2. 从Window菜单中选择Devices
  3. 在左边的DEVICES列表中选择一个device
  4. 在右边的界面中点击 "View Device Logs" 按钮
  5. 把崩溃报告拖入
  6. Xcode自动符号化并展现结果

为了能够符号化崩溃报告,Xcode需要定位到以下几个内容:

如果缺少以上的任何一个信息,都将不能符号化崩溃报告,或者仅能部分符号化崩溃报告。

2.4 通过atos符号化崩溃报告

atos命令可以把地址转换为相应的符号。如果全部的debug符号信息都是可用的,那么atos的输出将包含文件名和行数信息。使用atos来符号化部分日志报告:

  1. 找到你想符号化的堆栈信息中某一行。二进制映像的的名字在第二列,地址是在第三列。

  2. 从崩溃报告中末尾的二进制映像列表中找寻相应的二进制映像。


    tn2151_atos_info.png
  3. 通过UUID定位这个二进制映像的dSYM。

  4. 通过上述信息你能通过atos命令符号化相应的地址信息。

     atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>
    
3. 分析崩溃报告
3.1 Header

每个崩溃报告都有一个header。

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
3.2 异常信息

崩溃可以有很多Mach异常类型导致。不是所有的异常类型都会在崩溃报告中。

由于uncaught Objective-C exception引起的崩溃

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

接下去来介绍一下常见的异常类型:

Bad Memory Access [EXC_BAD_ACCESS // SIGSEGV // SIGBUS]
程序尝试进入无效的内存,或者尝试进入不被允许进入的内存。Exception Subtype字段包含kern_return_t用来描述错误和错误进入的内存地址。

下面是一些错误内存进入崩溃的调试技巧:

Abnormal Exit [EXC_CRASH // SIGABRT]
程序非正常退出。这种崩溃绝大多数由于uncaught Objective-C/C++异常和调用abort()引起。

Trace Trap [EXC_BREAKPOINT // SIGTRAP]
跟非正常退出类型。这个异常会打算给一个附加的调试器,使有机会在设定的点来中止程序。你能通过__builtin_trap()函数来触发这个异常。如果没有调试器被附加,程序会终止并产生崩溃报告。
底层库在遇到致命错误后会触发这个异常。关于这个错误的详细信息可以在下面的Additional Diagnostic Information中找到。
swifi代码在runtime时如果如下几种情况时也会产生这种异常:

Illegal Instruction [EXC_BAD_INSTRUCTION // SIGILL]
程序尝试执行一个非法的或者没有定义的指令。程序可能通过一个错误的函数指针进入一个无效的地址。

Quit [SIGQUIT]
程序被其他程序终止。在iOS中,键盘extension可能被宿主app杀死由于加载时间太长。

Killed [SIGKILL]
程序被系统终止。

Guarded Resource Violation [EXC_GUARD]
程序违反进入保护的资源。比如程序使用SQLite命令来操作Core Data,将会导致崩溃。

Resource Limit [EXC_RESOURCE]
程序超过了资源消耗的限制。

Other Exception Types
一些崩溃报告可能报告没有命名的异常类型,只会打印16进制数值。

注意:终止一个挂起的程序不会产生崩溃报告。

3.2 额外的诊断信息 (Additional Diagnostic Information)

链接的framework无法被找到引起的崩溃

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.

加载初始化view controller失败引起的崩溃

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
3.4 回溯

崩溃日志中最有趣的部分是程序终止时的每个线程回溯。

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

...

第一行列举了线程的序号和当前队列的标识。剩下的行表示回溯中的堆栈详情。从左到右依次为:

异常(Exceptions)

Objective-C中的异常是用来指明程序运行时检测到的错误的,比如数组越界,转换不可变对象为可变对象,没有实现一个必要的方法或者协议,或者给接收者发送一个无法识别的message。

注意:给已经释放的对象发送消息可能会引发NSInvalidArgumentException,而不是违反内存进入崩溃。发生这种情况常常是被创建的新对象的内存覆盖了先前已经释放对象的内存。如果你的程序由于没有捕获NSInvalidArgumentException引起崩溃,可以使用 Zombies instrument来解决引发这种错误内存管理的问题。

如果一个异常没有被捕获,它会被一个叫做uncaught exception handler的函数拦截。uncaught exception handler默认在设备的console打印异常信息,然后终止程序。这是崩溃报告中只会在Last Exception Backtrace节中有这个异常的回溯。详细的异常信息在崩溃报告中被遗漏。如果你收到一个含有Last Exception Backtrace的崩溃报告,你应该从原始射中中获取console日志来更好的了解当时引起异常的情况。

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的崩溃报告只会有16进制地址,它必须被符号化才能变得可读。

下面是符号化的日志报告中的Last Exception Backtrace节异常。这个异常是由于缺少IBOutlet在storyboard中引起的。

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

注意:如果你发现应用程序所指定的异常处理域的异常在抛出时没有被捕获,请检查你的应用在编译时是否设置了-no_compact_unwind标识。

64位的iOS系统使用了“零成本”异常实现。在“零成本”系统中,每个函数都有额外的数据来描述如何展开堆栈,在异常抛出时。如果抛出了一个没有展开数据的堆栈的异常,异常就无法被处理,然后程序被终止。异常处理程序可能能处理堆栈,但是如果没有展开数据的堆栈,那么就没有办法从异常中获取相应的堆栈。设置-no_compact_unwind标识意味着你不需要展开的数据,那么你就不能从那些函数中抛出异常。

此外,如果你的程序或者库中包含C代码,你需要指定-funwind-tables标识来让你的代码中的函数包含展开数据。

3.5 线程状态

这一节主要列举崩溃线程的线程状态。下面是程序终止时寄存器的值。在阅读崩溃报告时,不一定要了解线程状态,但是如果能够使用这些信息的话,就会更好的了解当时的崩溃状态。

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
3.6 二进制映象

这一节列举了程序终止时加载的二进制映象。

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

每一行列出二进制映象的一下细节:

4. 了解低内存报告

当低内存被检测到时,iOS上的虚拟内存系统会协调应用来释放内存。低内存通知被发送到所有正在运行的应用来处理释放内存的请求,希望来降低使用的内存的总量。如果内存压力依旧存在,系统可能终止在后台的程序来降低内存压力。如果足够的内存被释放掉,你的程序将继续运行。如果没有的话,你的程序将会被终止,因为没有足够的内存来满足程序的需求,同时会在设备上生成一个低内存报告。

低内存报告的形式于其他崩溃报告不同,它没有程序线程的回溯信息。低内存报告的header和崩溃报告的相同。需要注意page size这个字段。每个程序的内存使用都会在低内存报告中内存页数量的形式展示。

低内存报告中最重要的时程序表。程序表包含所有正在运行的程序,包括系统守护进程。如果一个程序被杀死,原因会在 [reason]列中展示。一个程序可能被杀死的原因有以下这么几个:

如果你没有看到原因中列出你的应用/扩展的进程,那么可能不是因为内存压力引起的崩溃。查看.crash文件(上一节讲述的)了解更多信息。

当你看到一个低内存崩溃时,与其关心那一部分代码在应用终止时正在执行,倒不如检查你对内存的使用方式和对低内存警告的响应处理。在你的应用中查找内存问题一文中详细地讲述了如何使用Instruments的Leaks分析来发现内存泄露,以及如何使用Allocations分析的Mark Heap功能来防止出现被遗弃的内存。《内存使用性能指南》论述了一种正确的方法来应对低内存通知,同时又提供了很多有效使用内存的技巧。同时也建议你看看WWDC2010年会议,使用Instruments进行高级内存分析

上一篇下一篇

猜你喜欢

热点阅读