应用崩溃是影响 APP 体验的重要一环, 而崩溃定位也常常让开发者头疼。本文就讲讲关于 Crash 分析的那些事。
1. Crash 日志的渠道
Crash 日志从哪来?一般有 2 个渠道:
-
苹果收集的 Crash 日志
-
- 在 Xcode -> Window -> Organizer -> Crashes 里面可以查看
-
- 用户手机上 设置 -> 隐私 -> 分析 里面的,可以连接电脑 Xcode 导出。
-
-
自己应用内收集的
-
- 接入
PLCrashReporter、KSCrash等 SDK 进行收集,上报到自建平台统计
- 接入
-
- 接入一些 APM 产品, 如 EMAS、mPaaS、phabricator 等。
-
两者各有利弊,但是二者的捕获原理是差不多的。
-
苹果的日志
- 优点: 理论上捕获类型最全,因为是
launchd进程捕获的日志。 - 缺点:不是全量日志,因为需要用户隐私授权才会上报,没有数据化支撑。
- 优点: 理论上捕获类型最全,因为是
-
自己收集的
- 优点:可以自建数据化支撑,获取 Crash 率等指标。
- 缺点:存在无法捕获的 Crash 的类型。
2. Crash 捕获的原理
要了解 Crash 捕获的原理,要先清楚几个基本概念,和它们之间的关系:
-
软件异常
- 软件异常主要来源于两个 API 的调用
kill()、pthread_kill(), 而 iOS 中我们常常遇到的NSException未捕获、abort()函数调用等,都属于这种情况。比如我们常看到 Crash 堆栈中有pthead_kill方法的调用。
- 软件异常主要来源于两个 API 的调用
-
硬件异常
- 硬件产生的信号始于处理器 trap,处理器 trap 是平台相关的。比如我们遇到的野指针崩溃大部分是硬件异常。
-
Mach异常
- 这里虽然叫异常,但是要和上面的两种分开来看。我们了解到苹果的内核
xnu的核心是Mach, 在Mach之上建立了BSD层。 “Mach异常” 是 “Mach异常处理流程” 的简称。
- 这里虽然叫异常,但是要和上面的两种分开来看。我们了解到苹果的内核
-
UNIX信号
- 这就是我们常说的信号了,如
SIGBUSSIGSEGVSIGABRTSIGKILL等。
- 这就是我们常说的信号了,如
3. Crash 异常码
在 Crash 头部信息之下, 会有个段记录了 Crash 异常码。类似下图:
这里我们应该关注:
Exception Type:异常码,一般格式是 Mach异常码 ( UNIX 信号类型 )Exception Subtype:一般情况里面带的是 Mach异常的 subcode, 还有 Crash 相关地址信息。Triggered by Thread:发生Crash的线程,大部分情况到这个线程的堆栈里面去看 Crash 堆栈。Application Specific Information:如果是 Objc/c++ Exception 异常,这里是异常的信息,这个是定位异常的关键信息Last Exception Backtrace:抛出异常的代码堆栈, 如果是 Objc/c++ Exception 异常造成的 Crash,就看这个堆栈,Crashed Thread:里的堆栈是 abort(),没有意义。
4.Crash文件
当你的应用提交到AppStore或者各个渠道之后,请问你多久会拿到crash文件?你如何分析crash文件的呢?
上传crash文件
你的应用应当有模块能够在应用程序crash的时候上传crash信息。 要么通过用户反馈拿到crash文件,要么借助自己或第3方的crash上传模块拿到crash文件。
今天要分析的场景是你拿到用户的.crash文件之后,如何符合化crash文件(Symbolicating crash logs)的3种方法。帮助尽快找到crash原因。
PS;作为一个iOS开发,都需要一个非常好的互相讨论的环境,而这欢迎你的加入(都懂的)
crash文件例子
crash文件的部分内容:
Last Exception Backtrace:
0 CoreFoundation 0x30acaf46 exceptionPreprocess + 126
1 libobjc.A.dylib 0x3af0b6aa objc_exception_throw + 34
2 CoreFoundation 0x30a0152e -[__NSArrayM objectAtIndex:] + 226
3 appName 0x000f462a 0x4000 + 984618
4 appName 0x00352aee 0x4000 + 3468014
…
18 appName 0x00009404 0x4000 + 21508
大家一眼就能看到:
- 2 CoreFoundation 0x30a0152e -[__NSArrayM objectAtIndex:] + 226
这一行有问题。
但是,第3行和第4行的:
3 appName 0x000f462a 0x4000 + 984618
4 appName 0x00352aee 0x4000 + 3468014
并没有指出到底是app的那个模块导致的问题,如何排查呢?
使用XCode
这种方法可能是最容易的方法了。
要使用Xcode符号化 crash log,你需要下面所列的3个文件:
- crash报告(.crash文件)
- 符号文件 (.dsymb文件)
- 应用程序文件 (appName.app文件,把IPA文件后缀改为zip,然后解压,Payload目录下的appName.app文件), 这里的appName是你的应用程序的名称。
把这3个文件放到同一个目录下,打开Xcode的Window菜单下的organizer,然后点击Devices tab,然后选中左边的Device Logs。
然后把.crash文件拖到Device Logs或者选择下面的import导入.crash文件。
这样你就可以看到crash的详细log了。 如下图:
使用命令行工具symbolicatecrash
有时候Xcode不能够很好的符号化crash文件。我们这里介绍如何通过symbolicatecrash来手动符号化crash log。
在处理之前,请依然将“.app“, “.dSYM”和 ".crash"文件放到同一个目录下。现在打开终端(Terminal)然后输入如下的命令:
export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
然后输入命令:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash appName.crash appName.app > appName.log
现在,符号化的crash log就保存在appName.log中了。
使用命令行工具atos
如果你有多个“.ipa”文件,多个".dSYMB"文件,你并不太确定到底“dSYMB”文件对应哪个".ipa"文件,那么,这个方法就非常适合你。
特别当你的应用发布到多个渠道的时候,你需要对不同渠道的crash文件,写一个自动化的分析脚本的时候,这个方法就极其有用。
什么是UUID
每一个可执行程序都有一个build UUID来唯一标识。Crash日志包含发生crash的这个应用(app)的 build UUID以及crash发生的时候,应用加载的所有库文件的[build UUID]。
那如何知道crash文件的UUID呢?
可以用:
grep "appName armv" *crash
或者
grep --after-context=2 "Binary Images:" *crash
可以得到类似如下的结果:
appName.crash-0x4000 - 0x9e7fff appName armv7 <8bdeaf1a0b233ac199728c2a0ebb4165> /var/mobile/Applications/A0F8AB29-35D1-4E6E-84E2-954DE7D21CA1/appName.crash.app/appName
(请注意这里的0x4000,是模块的加载地址,后面用atos的时候会用到)
如何找到app的UUID
可以使用命令:xcrun dwarfdump -–uuid <AppName.app/ExecutableName>
比如:
xcrun dwarfdump --uuid appName.app/appName
结果如下:
UUID: 8BDEAF1A-0B23-3AC1-9972-8C2A0EBB4165 (armv7) appName.app/appName
UUID: 5EA16BAC-BB52-3519-B218-342455A52E11 (armv7s) appName.app/appName
这个app有2个UUID,表明它是一个fat binnary。
它能利用最新硬件的特性,又能兼容老版本的设备。
对比上面crash文件和app文件的UUID,发现它们是匹配的
8BDEAF1A-0B23-3AC1-9972-8C2A0EBB4165
用atos命令来符号化某个特定模块加载地址
命令是:
atos [-o AppName.app/AppName] [-l loadAddress] [-arch architecture]
文章开头提到crash文件中有如下两行,
* 3 appName 0x000f462a 0x4000 + 984618
* 4 appName **0x00352aee** 0x4000 + 3468014
在执行了上面的
xcrun atos -o appName.app.dSYM/Contents/Resources/DWARF/appName -l 0x4000 -arch armv7
之后,输入如下地址:
0x00352aee
(crash文件中的第4行:4 appName **0x00352aee** 0x4000 + 3468014)
可以得到结果:
-[UIScrollView(UITouch) touchesEnded:withEvent:] (in appName) (UIScrollView+UITouch.h:26)
这样就找到了应用种到底是哪个模块导致的crash问题。
取决于;本文地址
本视频全程免费;iOS Crash分析视频