对于做移动App开发的来说,质量和体验都是很重要的。一个客户端应用如果经常“闪退”,是产品质量很差的一个体现,用户体验就更不用提了。所以开发一个优秀的App,首先是保证自身的技术质量,尽量杜绝“闪退”,也就是“Crash”。但客户端上线后,偶尔出现一个隐藏很深的bug也在所难免。我们所能做的就是尽可能的收集问题相关的信息,争取在将来的新版本中解决和改进。
一个App启动之后,用着用着就突然被iOS系统关闭,或者干脆就起不来,在打开的一瞬间关闭,这就是Crash,俗称“闪退”“崩溃”。
iOS上的App闪退有各种各样的原因,手机过热、响应超时、内存过低都是有可能的crash原因。但更多情况下是App程序自身的运行逻辑存在问题、缺陷。比如调用用了Objective-C对象根本不支持的方法(发送消息),非法内存访问,数组越界,参数不符合要求等。
这些问题在调试阶段,我们都可以很容易的通过断点和console中提供的信息快速定位并解决。
但对于已发布的App,如果想重现并利用上述办法来解决,恐怕会比较费时费事。
最有帮助最直接的办法就是根据出现问题时的闪退日志,分析和判断crash的原因,快速准确的定位和解决。
在iOS上运行的App出现crash的时候,通常会生成一个crash log,记载问题发生时的具体状况。开发者可以在iTunes Connect(相当于App Store后台)中特定App下找到收集上来的crash log。不过客户端用户可以选择不发送诊断信息,这样收集上来的信息就不一定是全面的。
不过开发者可以对exception和signal设置自定义的handler做额外处理,以收集现场信息。现在也有很多第三方的工具很流行,比如Crashlytics,国内的友盟等。
闪退日志里面包含了Crash发生的App、运行软硬件环境、发生时间、错误类型、方法调用异常栈、各线程状态、寄存器和内存信息。
而其中对我们开发人员来说意义最为重大的,可能就是异常线程的调用栈,例如:
Last Exception Backtrace: 0 CoreFoundation 0x18517e950 __exceptionPreprocess + 132 1 libobjc.A.dylib 0x1916841fc objc_exception_throw + 60 2 CoreFoundation 0x185085910 -[__NSDictionaryM setObject:forKey:] + 900 3 CrashDebugInfoTest 0x1000c2b90 0x1000bc000 + 27536 4 CrashDebugInfoTest 0x1000c28dc 0x1000bc000 + 26844 5 UIKit 0x1881bc55c -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 316 6 UIKit 0x1881bbf08 -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1564 7 UIKit 0x1881b59ec -[UIApplication _runWithURL:payload:launchOrientation:statusBarStyle:statusBarHidden:] + 772 8 UIKit 0x1881498cc -[UIApplication handleEvent:withNewEvent:] + 3316 9 UIKit 0x188148ad0 -[UIApplication sendEvent:] + 104 10 UIKit 0x1881b5044 _UIApplicationHandleEvent + 672 11 GraphicsServices 0x18ad63504 _PurpleEventCallback + 676 12 GraphicsServices 0x18ad63030 PurpleEventCallback + 48 13 CoreFoundation 0x18513e890 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 56 14 CoreFoundation 0x18513e7f0 __CFRunLoopDoSource1 + 444 15 CoreFoundation 0x18513ca14 __CFRunLoopRun + 1620 16 CoreFoundation 0x18507d6d0 CFRunLoopRunSpecific + 452 17 UIKit 0x1881b41c8 -[UIApplication _run] + 784 18 UIKit 0x1881aefdc UIApplicationMain + 1156 19 CrashDebugInfoTest 0x1000c2c5c 0x1000bc000 + 27740 20 libdyld.dylib 0x191c77aa0 start + 4
其中从第二列来看,很多是开发库中的调用,而关键在于其间我们自己的App方法调用。可惜有些时候,这关键的信息竟然全是16进制的数据,我们很难看懂。比如:
3 CrashDebugInfoTest 0x1000c2b90 0x1000bc000 + 27536
那么要从十六进制的地址码,得到我们代码中对应的方法调用,就需要结合调试信息对crash log进行符号化。
符号化的方法多种多样,从网上社区论坛和个人经验看来,至少有如下办法:
更有牛人,自己写了个复杂的脚本来解决这个问题。下面我介绍我常使用的两种方法,一个是利用atos,一个是充分利用Xcode自带的工具。其它的大家都可以到网上参看相关文章,一搜一大筐。
atos,就是address to symbol,把地址翻译成符号。上面那段我提到了,要想把十六进制的地址翻译为符号,需要调试信息。最好用的调试信息就是我们在每次给App打包时生成的dSYM文件。而atos最好用的方式就是:
atos -o XXX.app.dSYM/Contents/Resources/DWARF/XXX -l address0 targetAddress
其中:
除了atos外,我想介绍的另一个办法就是使用Xcode自带的crash log分析工具,在老版本的Xcode中是在Organizer里,在新版本里是在Devices中。
有的朋友可能会说,那里面显示的可还是十六进制的地址啊!那是因为它“没看到”App和dSYM文件啊。那怎么办?简单:
把App和dSYM放在一个目录中,并用mdimport把目录加入到Spotlight的索引中即可。
怎么样,这招是不是更快更好用?symbolicatecrash神马的就不需要了吧!
之前本人曾经以framework(iOS Universal Framework)的方式开发了好多SDK供别人用。可当使用了framework库的App闪退了的时候,即使是SDK中的逻辑问题,异常栈中显示的也是App的名字。
更重要的是,默认情况下,异常栈的最右一列根本没法符号化。
这是因为framework实际上是一种静态库,在Build App时,它已经完全“融入”了,静态链接到App产物中。而在framework生成的时候,调试信息已经被抽取掉了。
我们打开SDK的工程文件,在Build Settings里搜索Strip,会发现有好几个选项:
对于这个问题,我们只要在Strip Linked Product一项中选择No就行了。这样在Build出的SDK framework中,包的体积会变大,因为它容纳了本要去除掉的调试信息。
按我在之前的Blog的办法,我们看看在Mach-O文件中多了什么:
Debug Info
是的,正是DWARF格式的数据。DWARF是一种通用的调试信息格式,可以认为是Debugging With Attributed Records Format的缩写。感兴趣的可以前往:
http://www.dwarfstd.org
这样,关于Crash问题的解决方案和原理我就解释清楚了,欢迎大家拍砖!