前言
APP 的 Crash 一般会定位为严重问题,生产环境下的 APP 一般需要做到 Crash 率高于 99.8%。真实的生产环境可能十分复杂,可能存在极其恶劣的网络环境的影响,后台传输数据格式不准确,内存处理不当等,这些都可能引发 APP 的 Crash。开发环境下,很难做到完全容错,测试也几乎不可能覆盖所有场景。因此,行之有效的监测机制几乎是必然的。
Objective-C 的不安全性
在 Objective-C 中,有些数据结构或方法只能接收非空的值,如果我们在调用这些方法时,没有做好充分的判断,错误的传入一个空值,这时候编译阶段并不会给出警告,在程序的运行时才会直接崩溃。类似于这段代码:
NSData *data = [NSJSONSerialization dataWithJSONObject:array options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
在上述代码中,当变量 array == nil
时,程序即会崩溃。这类问题在开发中一般是由于数据的改变,没有做足够的容错处理。如果测试的覆盖率不全,极有可能在线上发生崩溃。
在 Swift 中,在给上述方法传入 array 之前,需要对 array 做出明确的为空判断, 否则编译器会直接告警,一般会采用可选绑定(if...let
)的形式对可选类型做判断,当能进入到对应的代码块,也就意味着 array 一定是有值的。
let array : [Any]? = ["Alex"]
if let array = array {
let data = try? JSONSerialization.data(withJSONObject: array, options: JSONSerialization.WritingOptions.prettyPrinted)
}
Swift 可以通过自身的一些语言特性让这些问题避免在开发阶段,但是 Objective-C 很难做到。对于 Objective-C 开发的程序,非常有必要做一定的 Crash 监听工作。当监听到这些信息之后,可以通过版本迭代或 HotPatch
的形式及时作出修复,避免用户的流失。
在我们的项目中应用的是 友盟统计日志+dYSM分析工具 的形式,可以有效的解决一部分 Carsh。友盟统计只需要引入较少的代码,并在合适的时机调用极少的代码,无侵入性,容易移除。
友盟统计Crash日志
友盟统计具有分析流量来源、用户属性、行为数据、错误分析等特性,在我们的产品中一般会接入用来统计程序的Crash日志。
上线的 APP 集成了友盟统计后,当发生崩溃时,会在友盟统计后台的错误分析模块监听到崩溃日志。一般会看到两种错误类型:
1. 明确指出了报错API
*** +[NSJSONSerialization dataWithJSONObject:options:error:]: value parameter is nil
(null)
((
0 CoreFoundation 0x2268f933 + 150
1 libobjc.A.dylib 0x21e2ae17 objc_exception_throw + 38
2 CoreFoundation 0x2268f861 + 0
3 Foundation 0x22f28341 + 84
4 fuzhuxian 0x80099 fuzhuxian + 508057
5 UIKit 0x27060bd9 + 68
6 UIKit 0x27061283 + 30
7 UIKit 0x26f577e3 + 1230
8 UIKit 0x26f5aa85 + 192
9 UIKit 0x26d38157 + 90
10 UIKit 0x26c45ba5 + 540
11 UIKit 0x26c45685 + 204
12 UIKit 0x26c4557f + 78
13 QuartzCore 0x24ca5689 + 252
14 libdispatch.dylib 0x221fd80f + 22
15 libdispatch.dylib 0x2220bba9 + 1524
16 CoreFoundation 0x22651b6d + 8
17 CoreFoundation 0x22650067 + 1574
18 CoreFoundation 0x2259f229 CFRunLoopRunSpecific + 520
19 CoreFoundation 0x2259f015 CFRunLoopRunInMode + 108
20 GraphicsServices 0x23b8fac9 GSEventRunModal + 160
21 UIKit 0x26c73189 UIApplicationMain + 144
22 fuzhuxian 0x29af9 fuzhuxian + 154361
23 libdyld.dylib 0x22247873 + 2
)
dSYM UUID: 679C384A-0751-3B66-8BAC-FB0DB78AC7B6
CPU Type: armv7
Slide Address: 0x00004000
Binary Image: fuzhuxian
Base Address: 0x0004b000
这一类的 Crash 明确的指出了崩溃的方法,上图中的问题是 NSJSONSerialization
对象的 dataWithJsonObject
方法传入了一个为空的参数,并且列出了报错的内存地址。类似的会打印出具体报错API的还有数组越界问题等。
2. Application received signal SIGSEGV
Application received signal SIGSEGV
(null)
((
0 CoreFoundation 0x21b47933 + 150
1 libobjc.A.dylib 0x212e2e17 objc_exception_throw + 38
2 CoreFoundation 0x21b47861 + 0
3 fuzhuxian 0x362d87 fuzhuxian + 3534215
4 libsystem_platform.dylib 0x2187606f _sigtramp + 34
5 Foundation 0x22365af5 __NSFireDelayedPerform + 468
6 CoreFoundation 0x21b0a58f + 14
7 CoreFoundation 0x21b0a1c1 + 936
8 CoreFoundation 0x21b0800d + 1484
9 CoreFoundation 0x21a57229 CFRunLoopRunSpecific + 520
10 CoreFoundation 0x21a57015 CFRunLoopRunInMode + 108
11 GraphicsServices 0x23047ac9 GSEventRunModal + 160
12 UIKit 0x2612b189 UIApplicationMain + 144
13 fuzhuxian 0x29c41 fuzhuxian + 154689
14 libdyld.dylib 0x216ff873 + 2
)
dSYM UUID: 27C7888F-E1F2-33DE-96ED-6031F25C66EC
CPU Type: armv7
Slide Address: 0x00004000
Binary Image: fuzhuxian
Base Address: 0x000f0000
这一类问题没有指出具体报错的方法,但是给出了可用来分析的内存地址 0x362d87
和 0x29c41
。出现这种问题的原因一般是程序访问了无效的内存。
实际上仅仅有这些报错的 API 或者 16 进制内存地址意义不大,因为依然不能反应出代码中的哪一个文件的哪一列出了错误。为了找出崩溃的具体位置,需要借助 dYSM 文件进行分析。
dYSM文件
dSYM 是保存 16 进制函数地址映射信息的中转文件,我们调试的 symbols 都会包含在这个文件中,并且每次编译项目的时候都会生成一个新的 dSYM 文件,位于 /Users/<用户名>/Library/Developer/Xcode/Archives
目录下。Xcode 编译项目后会看到一个同名的 dSYM 文件。
每一个 xx.app 和 xx.app.dSYM 文件都有对应的 UUID
,crash 文件也有自己的 UUID
,只要这三个文件的 UUID
一致,我们就可以通过他们解析出正确的错误函数信息了。因此,对于每一个发布版本我们都很有必要保存对应的 Archives
文件 。
dSYM分析工具
我们的项目中用到了 dSYMTools 作为分析工具分析 dSYM 文件,源码可以直接编译成 Mac APP。它的优点在于直接面向 .xcarchive
文件进行分析,不需要再去找对应的 dSYM 文件,系统会自动找到对应的 dSYM 文件。
可以直接将 achieve
出来的 .xcarchive
文件直接拖入到文件选择区域,选择对应的 CPU 类型(一般 iPhone5S 和 iPad Air 之后的是 arm64
类型),工具会默认匹配出可执行文件的 UDID
和 默认 Slide Address
,只需要再键入报错的内存地址,如上文代错误内存代码中的 0x80099
0x29af9
0x362d87
0x29c41
,点击分析,即可打印出可能出错的地方。
总结
对于每一个发布版本我们都很有必要保存对应的 Archives
文件 ,这个文件记录了
APP 的关键信息,可以作为日后分析问题的重要途径。
在生产环境下通过集成友盟或其他 SDK 的形式监测 Crash 报告,这实际是一种滞后的监听机制,它并不能实时的改变代码、修复线上 Bug,但是可以提升程序员的信心。对于开发者来说不失是一种有效的解决问题的方法。
Swift 可以在开发阶段有效的避免很大一部分上述问题 Xcode 自动的提示节省了一大部分思考的时间,让我们和你专注于业务逻辑的处理上,又保证了安全性,这对于开发者而言无非是一件好事。当然在写 Objective-C 时,基本的风险预估和容错处理也必不可少。