1、前言
闪退,指用户在使用app的过程中异常中断执行,被系统强制结束应用并回到桌面。不仅内存信息丢失,还会阻断用户操作流程,对业务影响及其严重。
当然,避免崩溃的最好办法就是不产生崩溃;在开发过各中就要尽可能地保证程序的健壮性。但是,人又不是机器,不可能不犯错,不可能存在没有BUG的程序。
如果能够利用一些语言机制和系统方法,设计一套防护系统,使之能够有效的降低APP的崩溃率,那么不仅APP的稳定性得到了保障,最重要的是可以减少不必要的加班。
当然我们不可能强大到把所有类型的crash都处理掉,但是我们会对一些高频的crash进行一一的处理,我们的目的就是降低crash率。
2、对RunTime的初识
C
语言作为一门静态类语言,在编译阶段就已经确定了所有变量的数据类型,同时也确定好了要调用的函数,以及函数的实现。
而Objective-C
语言是一门动态语言。在编译阶段并不知道变量的具体数据类型,也不知道所真正调用的哪个函数;在编译阶段,OC可以调用任何函数,即使这个函数只声明未实现,或直接未声明(performSelector:)
;只有在运行时间才检查变量的数据类型,同时在运行时才会根据函数名查找要调用的具体函数。这样在程序没运行的时候,我们并不知道调用一个方法具体会发生什么。
实现Objective-C
语言运行时机制的一切基础就是Runtime
。Runtime实际上是一个库,这个库使我们可以在程序运行时动态的创建对象、检查对象,修改类和对象的方法。
3、消息机制的基本原理
Objective-C
语言中,方法调用都是类似[receiver selector];
的形式,其本质就是让对象在运行时发送消息的过程。
我们来看看方法调用[receiver selector];
在『编译阶段』
和『运行阶段』
分别做了什么?
- 编译阶段:
[receiver selector];
方法被编译器转换为:
1.1.objc_msgSend(receiver,selector) (不带参数)
1.2.objc_msgSend(recevier,selector,org1,org2,…)(带参数)
OC代码示例:
id num = @123;
NSMutableString *str = [NSMutableString stringWithString:@"Hello"];
[str appendString:@"World"];
通过clang
命令查看OC代码编译后的样子:
//xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
id num = ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 123);
NSMutableString *str = ((NSMutableString * _Nonnull (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("stringWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_zh_hdyn1j_92zn2n74srg5dq88c0000gp_T_main_0eed7d_mi_1);
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_zh_hdyn1j_92zn2n74srg5dq88c0000gp_T_main_0eed7d_mi_2);
- 运行时阶段:消息接受者
recevier
寻找对应的selector
。
2.1、通过recevier
的isa
指针找到recevier
的Class(类)
;
2.2、在Class(类)
的cache(方法缓存)
的散列表中寻找对应的IMP(方法实现)
;
2.3、如果在cache(方法缓存)
中没有找到对应的IMP(方法实现)
的话,就继续在Class(类)
的method list(方法列表)
中找对应的selector
,如果找到,填充到cache(方法缓存)
中,并返回selector
;
2.4、如果在Class(类)
中没有找到这个selector
,就继续在它的superClass(父类)
中寻找,以此类推,一直查找到根类;
2.5、一旦找到对应的selector
,直接执行recevier
对应selector
方法实现的IMP(方法实现)
。
2.6、若找不到对应的selector
,转向拦截调用,走消息转发;如果没有重写拦截调用的方法,程序就会崩溃。
4、防护原理
利用Objective-C
语言的动态特性,采用AOP(Aspect Oriented Programming)
面向切面编程的设计思想,在不侵入原有项目代码的基础之上,通过在 APP 运行时阶段对崩溃因素的的拦截和处理,使得 APP 能够持续稳定正常的运行。
具象的说,就是对需要Hook的类添加Category(分类)
,在各个分类中通过Method Swizzling
拦截容易造成崩溃的系统方法,将系统原有方法与添加的防护方法的selector(方法选择器)
与IMP(函数实现指针)
进行对调。然后在替换方法中添加防护操作,从而达到避免以及修复崩溃的目的。
5、防护系统可以处理掉哪几种crash类型?
- unrecognized selector(找不到对象方法或者类方法的实现)
- KVO Crash
- KVC Crash
- NSNotification Crash
- NSTimer Crash (注册了没有主动释放会内存泄露,甚至在定时任务触发时可能会导致crash)
- Container / NSString Crash(集合类操作造成的崩溃,例如数组越界,插入 nil 等)
- Threading Crash (非主线程刷新UI)
5.1 unrecognized selector类型crash防护
unrecognized selector
类型的crash在app众多的crash类型中占着比较大的成分,通常是因为一个对象调用了一个不属于它方法的方法导致的。
部分复现代码:
//.h中声明了,但是没有实现此方法
[self gg];
//performSelector可以向一个对象传递任何消息,而不需要在编译的时候声明这些方法
[self performSelector:@selector(gg1)];
[self performSelector:NSSelectorFromString(@"gg2:")];
//id能代表任意类型对象,在编译期会跳过类型检查
id str = @123;
[str appendString:@"Hello World"];
//类型不匹配导致崩溃
[self gg3:@123];
- (void)gg3:(NSString *)str
{
//没有对参数进行类型判断
NSInteger length = str.length;
}
在3、消息机制的基本原理 最后一步中有提到:若找不到对应的selector,转向拦截调用,走消息转发;如果没有重写拦截调用的方法,程序就会崩溃
。
当一个方法找不到的时候,Runtime 提供了消息动态解析
、消息接受者重定向
、消息重定向
等三步处理消息,具体流程如下:
图解:
消息动态解析
:Objective-C
运行时会调用+resolveInstanceMethod:
或者 +resolveClassMethod:
,让你有机会提供一个函数实现。我们可以通过重写这两个方法,添加其他函数实现,并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。若返回NO或者没有添加其他函数实现,则进入下一步。
举个例子:
#import "ViewController.h"
#include <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 执行 fun 函数
[self performSelector:@selector(fun)];
}
// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(fun)) { // 如果是执行 fun 函数,就动态解析,指定新的 IMP
//特殊参数:v@:,具体可参考官方文档:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
class_addMethod([self class], sel, (IMP)funMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void funMethod(id obj, SEL _cmd) {
NSLog(@"funMethod"); //新的 fun 函数
}
控制台打印结果:
2021-03-30 16:50:06.145815+0800 CrashKiller_Example[22017:4869725] funMethod
从上边例子中可以看出,虽然我们没有实现 fun 方法,但是通过重写resolveInstanceMethod: ,
利用 class_addMethod 方法添加对象方法实现 funMethod 方法,并执行。
从打印结果来看,成功调起了funMethod 方法。
消息接受者重定向
:如果当前对象实现了forwardingTargetForSelector:
,Runtime
就会调用这个方法,允许我们将消息的接受者转发给其他对象。如果这一步方法返回 nil,则进入下一步。
举个例子:
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)fun;
@end
@implementation Person
- (void)fun {
NSLog(@"fun");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 执行 fun 方法
[self performSelector:@selector(fun)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 为了进行下一步 消息接受者重定向
}
// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(fun)) {
return [[Person alloc] init];
// 返回 Person 对象,让 Person 对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
控制台打印结果:
2021-03-30 17:10:52.645582+0800 CrashKiller_Example[22427:4885650] fun
可以看到,虽然当前 ViewController 没有实现 fun 方法+resolveInstanceMethod:也没有添加其他函数实现。
但是我们通过 forwardingTargetForSelector 把当前 ViewController 的方法转发给了 Person 对象去执行了。
打印结果也证明我们成功实现了转发。
消息重定向
:Runtime
系统利用methodSignatureForSelector:
方法获取函数的参数和返回值类型。
- 如果
methodSignatureForSelector:
返回了一个NSMethodSignature
对象(函数签名),Runtime
系统就会创建一个NSInvocation
对象,并通过forwardInvocation:
消息通知当前对象,给予此次消息发送最后一次寻找IMP
的机会。 - 如果
methodSignatureForSelector:
返回 nil。则Runtime
系统会发出doesNotRecognizeSelector:
消息,程序也就崩溃了。
举个例子:
#import "ViewController.h"
#include "objc/runtime.h"
@interface Person : NSObject
- (void)fun;
@end
@implementation Person
- (void)fun {
NSLog(@"fun");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 执行 fun 函数
[self performSelector:@selector(fun)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 为了进行下一步 消息接受者重定向
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil; // 为了进行下一步 消息重定向
}
// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector; // 从 anInvocation 中获取消息
Person *p = [[Person alloc] init];
if([p respondsToSelector:sel]) { // 判断 Person 对象方法是否可以响应 sel
[anInvocation invokeWithTarget:p]; // 若可以响应,则将消息转发给其他对象处理
} else {
[self doesNotRecognizeSelector:sel]; // 若仍然无法响应,则报错:找不到响应方法
}
}
@end
控制台打印结果:
2021-03-30 17:26:26.719413+0800 CrashKiller_Example[22715:4896625] fun
从补救三部曲中,选择哪一步作为入手点最合适?
1. resolveInstanceMethod
需要在类的本身动态的添加它本身不存在的方法,这些方法对于该类本身来说是冗余的
2. forwardInvocation
可以通过NSInvocation
的形式将消息转发给多个对象,但是其开销比较大,需要创建新的NSInvocation
对象,并且forwardInvocation
的函数经常被使用者调用来做消息的转发选择机制,不适合多次重写。
3. forwardingTargetForSelector
可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写。
具体步骤如下:
1. 给NSObject
添加一个分类,在分类中实现一个自定义的-crashKiller_forwardingTargetForSelector:
方法;
2. 利用Method Swizzling
将-forwardingTargetForSelector:
和 -crashKiller_forwardingTargetForSelector:
进行方法交换。
3. 在自定义的方法中,先判断当前对象是否已经实现了消息接受者重定向
和消息重定向
。如果都没有实现,就动态创建一个目标类,给目标类动态添加一个方法。
4. 把消息转发给动态生成类的实例对象,由目标类动态创建的方法实现,这样 APP 就不会崩溃了。
实现代码如下:
#import "NSObject+KillSelector.h"
#import "NSObject+CrashKillerMethodSwizzling.h"
@implementation NSObject (KillSelector)
+ (void)registerKillSelector
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 拦截 `+forwardingTargetForSelector:` 方法,替换自定义实现
[NSObject crashKillerMethodSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(crashKiller_forwardingTargetForSelector:)
withClass:[NSObject class]];
// 拦截 `-forwardingTargetForSelector:` 方法,替换自定义实现
[NSObject crashKillerMethodSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(crashKiller_forwardingTargetForSelector:)
withClass:[NSObject class]];
});
}
+ (id)crashKiller_forwardingTargetForSelector:(SEL)aSelector {
if ([[CrashKillerManager shareManager].selectorClassWhiteList containsObject:[self class]]) {
return [self crashKiller_forwardingTargetForSelector:aSelector];
}
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 获取 NSObject 的消息转发方法
Method root_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
// 获取 当前类 的消息转发方法
Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);
// 判断当前类本身是否实现第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
// 如果没有实现第二步:消息接受者重定向
if (!realize) {
// 判断有没有实现第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method root_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
// 如果没有实现第三步:消息重定向
if (!realize) {
// 创建一个新类
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSString *reason = [NSString stringWithFormat:@"-[%@ %@]: unrecognized selector sent to class %p",errClassName,errSel,self];
[[CrashKillerManager shareManager] throwExceptionWithName:@"NSInvalidArgumentException" reason:reason];
NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);
// 如果类不存在 动态创建一个类
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注册类
objc_registerClassPair(cls);
}
// 如果类没有对应的方法,则动态添加一个
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)CrashIMP, "@@:@");
}
// 把消息转发到当前动态生成类的实例对象上
return [[cls alloc] init];
}
}
return [self crashKiller_forwardingTargetForSelector:aSelector];
}
- (id)crashKiller_forwardingTargetForSelector:(SEL)aSelector {
if ([[CrashKillerManager shareManager].selectorClassWhiteList containsObject:[self class]]) {
return [self crashKiller_forwardingTargetForSelector:aSelector];
}
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 获取 NSObject 的消息转发方法
Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
// 获取 当前类 的消息转发方法
Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
// 判断当前类本身是否实现第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
// 如果没有实现第二步:消息接受者重定向
if (!realize) {
// 判断有没有实现第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
// 如果没有实现第三步:消息重定向
if (!realize) {
// 创建一个新类
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSString *reason = [NSString stringWithFormat:@"-[%@ %@]: unrecognized selector sent to instance %p",errClassName,errSel,self];
[[CrashKillerManager shareManager] throwExceptionWithName:@"NSInvalidArgumentException" reason:reason];
NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);
// 如果类不存在 动态创建一个类
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注册类
objc_registerClassPair(cls);
}
// 如果类没有对应的方法,则动态添加一个
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)CrashIMP, "@@:@");
}
// 把消息转发到当前动态生成类的实例对象上
return [[cls alloc] init];
}
}
return [self crashKiller_forwardingTargetForSelector:aSelector];
}
// 动态添加的方法实现
static int CrashIMP(id slf, SEL selector) {
return 0;
}
@end
5.2 KVO Crash类型crash防护
KVO(Key-Value Observing),它提供一种机制,当指定的对象的属性被修改后,KVO就会自动通知相应的观察者。
通常一个对象的KVO关系图如下:
[self.wkWebView addObserver:self
forKeyPath:@"estimatedProgress"
options:NSKeyValueObservingOptionNew
context:nil];
[self.wkWebView addObserver:self
forKeyPath:@"title"
options:NSKeyValueObservingOptionNew
context:nil];
KVO Crash常见原因:
1. KVO 添加次数和移除次数不匹配:
• 移除未注册的观察者,导致崩溃
• 重复移除多次,移除次数多于添加次数,导致崩溃
• 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
2. 被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO,导致崩溃。 例如:被观察者是局部变量的情况(iOS 10 及之前会崩溃)。
3. 添加了观察者,但未实现 observeValueForKeyPath:ofObject:change:context: 方法,导致崩溃。
4. 添加或者移除时 keypath == nil,导致崩溃。
解决方案:
1. 首先为 NSObject 建立一个分类,利用 Method Swizzling,实现自定义的 crashKiller_addObserver:forKeyPath:options:context:
、crashKiller_removeObserver:forKeyPath:
crashKiller_removeObserver:forKeyPath:context:
、
crashKiller_dealloc
方法,用来替换系统原生的添加移除观察者方法的实现。
2. 然后在观察者和被观察者之间建立一个 KVOProxy对象,两者之间通过KVOProxy对象建立联系。然后在添加和移除操作时,将 KVO 的相关信息例如 observer、keyPath保存进关系哈希表kvoInfoMap中。 关系哈希表的数据结构:{keypath : observer1, observer2 , ...}
3. 在添加和移除操作的时候,利用KVOProxy对象做转发,把真正的观察者变为 KVOProxy对象,而当被观察者的特定属性发生了改变,再由 KVOProxy对象 分发到原有的观察者上。
添加观察者时: 通过关系哈希表判断是否重复添加,只添加一次。
移除观察者时: 通过关系哈希表是否已经进行过移除操作,避免多次移除。
观察键值改变时: 同样通过关系哈希表判断,将改变操作分发到原有的观察者上。
另外,为了避免被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO 导致崩溃。防护系统还利用 Method Swizzling 实现了自定义的 dealloc,在系统 dealloc 调用之前,将多余的观察者移除掉。
KVO崩溃测试代码:
/**
1.1 移除了未注册的观察者,导致崩溃
*/
- (void)testKVOCrash11 {
// 崩溃日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
[self.objc removeObserver:self forKeyPath:@"name"];
}
/**
1.2 重复移除多次,移除次数多于添加次数,导致崩溃
*/
- (void)testKVOCrash12 {
// 崩溃日志:Cannot remove an observer XXX for the key path "xxx" from XXX because it is not registered as an observer.
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.objc.name = @"0";
[self.objc removeObserver:self forKeyPath:@"name"];
[self.objc removeObserver:self forKeyPath:@"name"];
}
/**
1.3 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
*/
- (void)testKVOCrash13 {
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.objc addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.objc.name = @"0";
}
/**
2. 被观察者 dealloc 时仍然注册着 KVO,导致崩溃
*/
- (void)testKVOCrash2 {
// 崩溃日志:An instance xxx of class xxx was deallocated while key value observers were still registered with it.
// iOS 10 及以下会导致崩溃,iOS 11 之后就不会崩溃了
CrashObject *obj = [[CrashObject alloc] init];
[obj addObserver: self
forKeyPath: @"name"
options: NSKeyValueObservingOptionNew
context: nil];
obj = nil;
}
/**
3. 观察者没有实现 -observeValueForKeyPath:ofObject:change:context:导致崩溃
*/
- (void)testKVOCrash3 {
// 崩溃日志:An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
CrashObject *obj = [[CrashObject alloc] init];
[self addObserver: obj
forKeyPath: @"title"
options: NSKeyValueObservingOptionNew
context: nil];
self.title = @"111";
}
/**
4. 添加或者移除时 keypath == nil,导致崩溃。
*/
- (void)testKVOCrash4 {
// 崩溃日志: -[__NSCFConstantString characterAtIndex:]: Range or index out of bounds
CrashObject *obj = [[CrashObject alloc] init];
[self addObserver: obj
forKeyPath: @""
options: NSKeyValueObservingOptionNew
context: nil];
[self removeObserver:obj forKeyPath:@""];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context {
NSLog(@"object = %@, keyPath = %@", object, keyPath);
}
5.3 KVC Crash类型crash防护
KVC(Key Value Coding),即键值编码,提供一种机制来间接访问对象的属性。而不是通过调用Setter
、Getter
方法进行访问。
KVC 日常使用造成崩溃的原因通常有以下几个:
1. key 不是对象的属性,造成崩溃。
2. keyPath 不正确,造成崩溃。
3. key 为 nil,造成崩溃。
4. value 为 nil,为非对象设值,造成崩溃。
常见的使用 KVC 造成崩溃代码:
/********************* CrashObject.h 文件 *********************/
#import <Foundation/Foundation.h>
@interface CrashObject : NSObject
@property (nonatomic, copy) NSString *name;
@end
/********************* ViewController.m 文件 *********************/
#import "ViewController.h"
#import "CrashObject.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 1. key 不是对象的属性,造成崩溃
// [self testKVCCrash1];
// 2. keyPath 不正确,造成崩溃
// [self testKVCCrash2];
// 3. key 为 nil,造成崩溃
// [self testKVCCrash4];
// 4. value 为 nil,为非对象设值,造成崩溃
// [self testKVCCrash4];
}
/**
1. key 不是对象的属性,造成崩溃
*/
- (void)testKVCCrash1 {
// 崩溃日志:[<KVCCrashObject 0x600000d48ee0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key XXX.;
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:@"value" forKey:@"address"];
}
/**
2. keyPath 不正确,造成崩溃
*/
- (void)testKVCCrash2 {
// 崩溃日志:[<KVCCrashObject 0x60000289afb0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key XXX.
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:@"后厂村路" forKeyPath:@"address.street"];
}
/**
3. key 为 nil,造成崩溃
*/
- (void)testKVCCrash3 {
// 崩溃日志:'-[KVCCrashObject setValue:forKey:]: attempt to set a value for a nil key
NSString *keyName;
// key 为 nil 会崩溃,如果传 nil 会提示警告,传空变量则不会提示警告
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:@"value" forKey:keyName];
}
/**
4. value 为 nil,造成崩溃
*/
- (void)testKVCCrash4 {
// 崩溃日志:[<KVCCrashObject 0x6000028a6780> setNilValueForKey]: could not set nil as the value for the key XXX.
// value 为 nil 会崩溃
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:nil forKey:@"name"];
}
@end
KVC的setter和getter方法
Setter方法
系统在执行setValue:forKey:
方法时,会把key和value作为输入参数,并尝试在接收调用对象的内部,给属性key设置value值。
通过以下几个步骤:
`官方说明请查看 NSKeyValueCoding.h 文件`
1. 按顺序查找名为 `set<Key>: `、`_set<Key>:`、 `setIs<Key>:`方法。如果找到方法,则执行该方法,使用输入参数设置变量,则`setValue:forKey:`完成执行。如果没找到方法,则执行下一步。
2. 访问类的`accessInstanceVariablesDirectly`属性。如果 `accessInstanceVariablesDirectly`属性返回**YES**,就按顺序查找名为 `_<key>`、`_is<Key>`、`<key>`、`is<Key>`的实例变量,如果找到了对应的实例变量,则使用输入参数设置变量。则`setValue:forKey:`完成执行。如果未找到对应的实例变量,或者`accessInstanceVariablesDirectly`属性返回**NO**则执行下一步。
3. 调用`setValue: forUndefinedKey:`方法,并引发崩溃。
- 相关代码:
CrashObject *objc = [[CrashObject alloc] init];
[objc setValue:@"value" forKey:@"name"];
Getter方法
系统在执行valueForKey:
方法时,会将给定的key作为输入参数,在调用对象的内部进行以下几个步骤:
1. 按顺序查找名为get<Key>
、<key>
、is<Key>
、_<key>
的访问方法。如果找到,调用该方法,并继续执行步骤 5。否则继续向下执行步骤 2。
2. 搜索形如countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
的方法。
3. 如果实现了countOf<Key>
方法,并且实现了objectIn<Key>AtIndex:
和<key>AtIndexes:
这两个方法的任意一个方法,系统就会以NSArray为父类,动态生成一个类型为NSKeyValueArray的集合类对象,并调用上边的实现方法,将结果直接返回。
- 如果对象还实现了形如get<Key>:range:
的方法,系统也会在必要的时候自动调用。
- 如果上述操作不成功则继续向下执行步骤 3。
- 如果上边两步失败,系统就会查找形如countOf<Key>
、 enumeratorOf<Key>
、 memberOf<Key>:
的方法。系统会自动生成一个 NSSet类型的集合类对象,该对象响应所有NSSet方法并将结果返回。如果查找失败,则执行步骤 4。
4. 如果上边三步失败,系统就会访问类的accessInstanceVariablesDirectly
方法。
- 如果返回YES,就按顺序查找名为_<key>
、 _is<Key>
、 <key>
、 is<Key>
的实例变量。如果找到了对应的实例变量,则直接获取实例变量的值。并继续执行步骤 5。
- 如果返回NO,或者未找到对应的实例变量,则继续执行步骤 6。
5. 分为三种情况:
- 如果检索到的属性值是对象指针,则直接返回结果。
- 如果检索到的属性值是NSNumber支持的基础数据类型,则将其存储在 NSNumber 实例中并返回该值。
- 如果检索到的属性值是NSNumber不支持的数据类型,则转换为 NSValue 对象并返回该对象。
6. 如果一切都失败了,调用valueForUndefinedKey:
,并引发崩溃。
KVC Crash 防护方案
从 Setter 方法 和 Getter 方法 可以看出:
- setValue:forKey:
执行失败会调用setValue: forUndefinedKey:
方法,并引发崩溃。
- valueForKey:
执行失败会调用valueForUndefinedKey:
方法,并引发崩溃。
所以,为了进行 KVC Crash 防护,我们就需要重写setValue: forUndefinedKey:
方法和valueForUndefinedKey:
方法。重写这两个方法之后,就可以防护1. key不是对象的属性和2. keyPath不正确这两种崩溃情况了。
那么3. key为nil,造成崩溃的情况,该怎么防护呢?
我们可以利用 Method Swizzling方法,在NSObject的分类中将 setValue:forKey:
和crashKiller_setValue:forKey:
进行方法交换。然后在自定义的方法中,添加对key为nil这种类型的判断。
还有最后一种4. value为nil,为非对象设值,造成崩溃 的情况。
在NSKeyValueCoding.h
文件中,有一个setNilValueForKey:
方法。上边的官方注释给了我们答案。
在调用setValue:forKey:
方法时,系统如果查找到名为set<Key>:
方法的时候,会去检测value的参数类型,如果参数类型为NSNmber的标量类型或者是 NSValue的结构类型,但是value为nil时,会自动调用 setNilValueForKey:
方法。这个方法的默认实现会引发崩溃。
所以为了防止这种情况导致的崩溃,我们可以通过重写setNilValueForKey:
来解决。
至此,上文提到的 KVC 使用不当造成的四种类型崩溃就都解决了。
下面我们来看下具体实现代码:
+ (void)registerKillKVC
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[NSObject crashKillerMethodSwizzlingInstanceMethod:@selector(setValue:forKey:)
withMethod:@selector(crashKiller_setValue:forKey:)
withClass:[NSObject class]];
});
}
- (void)crashKiller_setValue:(id)value forKey:(NSString *)key {
@try {
[self crashKiller_setValue:value forKey:key];
} @catch (NSException *exception) {
[[CrashKillerManager shareManager] printLogWithException:exception];
}
}
- (void)setNilValueForKey:(NSString *)key {
NSString *reason = [NSString stringWithFormat:@"[<%@ %p> setValue:forUndefinedKey:]: could not set nil as the value for the key %@. ***",NSStringFromClass([self class]),self,key];
[[CrashKillerManager shareManager] throwExceptionWithName:@"NSUnknownKeyException" reason:reason];
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSString *reason = [NSString stringWithFormat:@"[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key %@.",NSStringFromClass([self class]),self,key];
[[CrashKillerManager shareManager] throwExceptionWithName:@"NSUnknownKeyException" reason:reason];
}
- (nullable id)valueForUndefinedKey:(NSString *)key {
NSString *reason = [NSString stringWithFormat:@"[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key %@.",NSStringFromClass([self class]),self,key];
[[CrashKillerManager shareManager] throwExceptionWithName:@"NSUnknownKeyException" reason:reason];
return self;
}
5.4 NSNotification Crash类型crash防护
当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。
NSNotification类型的crash多产生于程序员写代码时候犯疏忽,在NSNotificationCenter添加一个对象为observer之后,忘记了在对象dealloc的时候移除它。
所幸的是,苹果在iOS9之后专门针对于这种情况做了处理,所以在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。
5.5 NSTimer Crash类型crash防护
NSTimer crash 产生原因:
在程序开发过程中,大家会经常使用定时任务,但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
接口做重复性的定时任务时存在一个问题:NSTimer会强引用target实例,所以需要在合适的时机invalidate定时器,否则就会由于定时器timer强引用target的关系导致target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash。 crash的展现形式和具体的target执行的selector有关。
与此同时,如果NSTimer是无限重复的执行一个任务的话,也有可能导致target的selector一直被重复调用且处于无效状态,对app的CPU,内存等性能方面均是没有必要的浪费。
所以,很有必要设计出一种方案,可以有效的防护NSTimer的滥用问题。
NSTimer crash 防护方案:
上面的分析可见,NSTimer所产生的问题的主要原因是因为其没有再一个合适的时机invalidate,同时还有NSTimer对target的强引用导致的内存泄漏问题。
那么解决NSTimer的问题的关键点在于以下两点:
1. NSTimer对其target是否可以不强引用。
2. 是否找到一个合适的时机,在确定NSTimer已经失效的情况下,让NSTimer自动invalidate。
关于第一个问题,target的强引用问题,可以用如下图的方案来解决:
在NSTimer和target之间加入一层stubTarget,stubTarget主要做为一个桥接层,负责NSTimer和target之间的通信。同时NSTimer强引用stubTarget,而stubTarget弱引用target,这样target和NSTimer之间的关系也就是弱引用了,意味着target可以自由的释放,从而解决了循环引用的问题。
上文提到了stubTarget负责NSTimer和target的通信,其具体的实现过程又细分为两大步:
1. swizzle NSTimer中scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
相关的方法,在新方法中动态创建stubTarget对象,stubTarget对象弱引用持有原有的target,selector,timer,targetClass等properties。然后将原target分发stubTarget上,selector回调函数为stubTarget的fireProxyTimer;
2. 通过stubTarget的fireProxyTimer:来具体处理回调函数selector的处理和分发,当NSTimer的回调函数fireProxyTimer:被执行的时候,会自动判断原target是否已经被释放,如果释放了,意味着NSTimer已经无效,此时如果还继续调用原有target的selector很有可能会导致crash,而且是没有必要的。所以此时需要将NSTimer invalidate。
如此一来就做到了NSTimer在合适的时机自动invalidate。
5.6 Container / NSString 类型crash防护
这个问题比较简单,就是对一些常用类中会导致崩溃的API进行method swizzling,然后在swizzle的新方法中加入一些条件限制和判断,从而让这些API变的安全。
目前对以下类进行防范,类中可能导致crash的方法,逐步进行增量扩充。
NSArray/NSMutableArray
NSDictionary/NSMutableDictionary
NSString / NSMutableString
5.7 非主线程刷UI类型crash防护(UI not on Main Thread)
初步处理方案就是swizzle UIView类的以下三个方法:
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
在这三个方法调用的时候判断 一下当前的线程,如果不是主线程的话,直接利用dispatch_async(dispatch_get_main_queue(), ^{ //调用原本方法 });
来将对应的刷新UI的操作转移到主线程。
考虑到此三个方法调用频率过高,且在开发阶段xcode就会给出提示;故不考虑维护此类异常crah。