Crash 分析与治理是开发过程的必备技能,Crash 相关数据也是衡量应用程序质量的重要指标。
本文总结了 iOS 开发中基础的 Crash 种类以及防护方案。
常见崩溃及防护
1.集合类相关的 Crash
这类Crash 主要是对 OC 中的集合类(如 NSArray/NSMutableArray/NSDictionary/NSMutableDictionary)的不当操作产生的崩溃。
常见场景
- 数组越界
- 向数组、字典中添加 nil 元素
- 在遍历数组的过程中,使用错误的方法修改了数组
防护措施
可通过 swizzle 系统相关的方法,在自定义的方法中加一些条件判断,从而防止崩溃。
/**
NSArray 防越界取出NSArray
*/
- (id)safe_objectAtIndex:(NSUInteger)index {
if (index >= self.count){
return nil;
}
return [self safe_objectAtIndex:index];
}
/**
NSMutableArray 插入 新值 到 索引index 指定位置
*/
- (void)safeMutable_insertObject:(id)anObject atIndex:(NSUInteger)index {
if (index > self.count) {
return;
}
if (!anObject){
return;
}
[self safeMutable_insertObject:anObject atIndex:index];
}
2.KVC
KVC(Key Value Coding)又称键值编码,开发者可以通过 key 名直接访问对象的属性或给属性赋值,而不需要调用存取方法。常见的一些 Crash 场景有:
常见场景
- key 的值为 nil
- key 不是对象的属性或变量
- keyPath 不正确
- value 为nil,为非对象赋值
防护措施
// 针对 key 不正确的情况,重写如下两个方法
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key: %@,value:%@'",NSStringFromClass([self class]),self,key,value];
NSLog(@"%@", crashMessages);
}
- (nullable id)valueForUndefinedKey:(NSString *)key {
NSString *crashMessages = [NSString stringWithFormat:@"crashMessages :[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key: %@",NSStringFromClass([self class]),self,key];
NSLog(@"%@", crashMessages);
return self;
}
//处理 key 为 nil 的情况,swizzle 系统 setValu 方法
- (void)xx_setValue:(id)value forKey:(NSString *)key {
if (key == nil) {
NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setNilValueForKey]: could not set nil as the value for the key %@.",NSStringFromClass([self class]),self,key];
NSLog(@"%@", crashMessages);
return;
}
[self ysc_setValue:value forKey:key];
}
//处理 value 为 nil的情况
- (void)setNilValueForKey:(NSString *)key {
NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setNilValueForKey]: could not set nil as the value for the key %@.",NSStringFromClass([self class]),self,key];
NSLog(@"%@", crashMessages);
}
3.KVO
KVO(Key Value Observing),是一套事件通知机制。允许一个对象监听另一个对象属性的变化。对于被观察者和观察者之间的关系是一对一的,不是一对多的。在某些场景下如果使用不当也会产生 Crash。
常见场景
- 观察者 或 被观察者是局部对象
- 添加或移除时,keyPath 为nil
- 即添加次数和移除次数不匹配(重复移除监听对象)
- 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context: 方法
防护措施
防护方案的主要思路是:
- 创建一个 KVOProxy 对象,并通过哈希表维护 oberver 与 keyPath 之间的关系,存储的结构是:{ keyPath: [observer1, observer2,...] } (记录被观察者个数,防护添加、移除次数不匹配的情况)
- swizzle NSObject 的添加、移动观察者等方法,将观察者设置成 KVOProxy 对象,然后再利用 KVOProxy 对象进行分发处理(防护 keyPath为 nil 的情况,以及防护未实现 observeValueForKeyPath 方法的情况)
- swizzle NSObject 的 dealloc 方法,在此移除多余的观察者(防止局部变量提前释放导致的 Crash)
4.Unrecognized Selector (未实现的方法)
如果对象没有方法的实现,或者对象没有该方法,导致的崩溃。常见的场景有:
常见场景
- .h声明了方法,.m却没有实现
- 协议中的方法没有实现
if ([self.delegate respondsToSelector:@selector(protocolMehtod)]) {
[self.delegate protocolMehtod];
}
- copy 修饰可变属性
@property (nonatomic, copy) NSMutableArray *mutableArray;
- 动态调用未知方法
if ([self respondsToSelector:@selector(unknow)]) {
[self performSelector:@selector(unknow)];
}
- 低版本调用了高版本的 API
if (@available(iOS 13.0, *)) {
[self canPerformUnwindSegueAction:@selector(test) fromViewController:self sender:nil];
} else {
// Fallback on earlier versions
}
防护措施
当找不到对象的方法实现时,会进入到消息的转发流程,可以在这里进行拦截并处理。
消息转发流程:
- 动态方法解析
首先调用 +resolveInstanceMethod 方法,查看是否有动态添加的方法实现。
- 备用的接受者
上一步失败,调用 forwardingTargetForSelector 查看是否有其他对象处理了该消息。
- 完成的消息转发
前两步都失败了,则启动完整的消息转发机制,调用 forwardingInvocation,通过封装完整的 NSInvocation, 明确指出方法的响应者。
-
以上步骤都失败,则抛出异常。
防护措施的思路是:
-
swizzle NSObject 的 forwardingTargetForSelector 方法
-
判断当前对象是否已经实现了消息转发中的第二步、第三步的方法,如果都没有实现,就动态的创建一个目标类,给目标类动态添加一个方法
-
返回动态创建的目标类的实例对象。
5.Bad Access (野指针)
野指针通常指所指向的对象已经被释放的指针,其所指向的内存地址存储的数据也被称为僵尸对象。野指针相关的异常场景有:
常见场景
- ARC 下,使用 assign 或 unsafe_unretained 修饰对象
- runtime 关联对象使用了不合适的修饰符,如 OBJC_ASSOCIATION_ASSIGN
- 使用了未初始化的对象
防护措施
可在开发阶段,通过开启 xcode 的僵尸对象功能进行检测和预防
6.多线程相关 Crash
多线程产生的异常和野指针一样都不是很容易复现的,因此在写代码的时候一定要注意开发规范,常见问题的场景有:
常见场景
- 非主线程刷新 UI
如:在子线程调用了 UI 刷新相关的操作;如果需要刷新 UI,可通过 dispatch_async(dispatch_get_main_queue(), ^{ //调用UI 相关方法 }) 回到主线程执行相关操作。
- 多线程同时操作同一个数组
如:当一个线程正在遍历数组,此时另一个线程改变了数组元素的个数,会由于索引错乱而产生Crash。当多线程遍历数组时,可先拷贝一份再进行遍历。
- 多线程死锁
如:group enter 与 group leave 需要保证成对调用,否则很容易出现死锁问题。
7.watch dog 异常
为防止一个应用占用过多的系统资源,Apple 设计了一个名为“看门狗”(watch dog)的机制。如果某个场景超过了所规定的运行时间,“看门狗”就会强制终结这个应用程序。异常代码为:“0x8badf00d” (很像 bad food)。
Watch dog 造成的 Crash 不是代码本身的错误,其实是一种保护机制。当收集到这类的异常问题时,可着重考虑下应用的性能(长卡)或者是否有死锁等异常逻辑。
常见场景
-
长卡导致
-
死锁导致
如何收集 Crash 日志?
Crash 日志的来源一般有两个渠道:
苹果收集的
- 用户的 iPhone:可从 “设置 - 隐私 - 分析与改进 - 分析数据” 中获得
- 若 App 绑定了 Apple Connect 账号,可从 Xcode - Window - Origanizer - Crashes 查看
应用内自己收集的
-
接入 APM 产品, 如 Bugly、EMAS、phabricator 等
-
接入 SDK 收集,上报到自己的平台统计,如 PLCrashReporter 、 KSCrash 等
如何确认 Crash 文件、App包、dSYM 三者 UUID 一致?
Crash 文件中的 UUID
如上图,Crash 文件中二进制文件信息在 Binary Images 中列出,分别是二进制文件名(TouchCanvas)、CPU架构(arm64)、二进制文件UUID
APP 包的 UUID
- 把APP的后缀名ipa改为zip,并解压,得到后缀名为app的二进制文件。
- 使用dwarfdump -uuid XXX.app/XXX 查看APP所包含的CPU架构的UUID,以及对应的架构,如图:
dSYM 的 UUID
与APP 一样,使用 dwarfdump -uuid XXX.app.dSYM 查看dSYM所包含的CPU架构的UUID:
如何符号化?
- symbolicatecrash
- atos
- CrashSymbolicator
可参考:iOS 崩溃符号化工具
总结
以上列举了开发中常见的一些 Crash,以及利用 Objective-C 语言的动态性,拦截容易造成崩溃的系统方法,添加一些防护操作,从而达到避免以及修复崩溃的目的。
但是防护的目的不是掩饰,一定要在关键节点将拦截到的异常信息记录或者上报,从而对导致异常的原因完成真正意义上的修复。
参考 Crash分析实践