iOS-unrecognized selector crash防护
Posted 想名真难
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS-unrecognized selector crash防护相关的知识,希望对你有一定的参考价值。
避免崩溃问题的最好办法就是不产生崩溃。在开发的过程中就要尽可能地保证程序的健壮性。但是,人又不是机器,不可能不犯错。不可能存在没有 BUG 的程序。但是如果能够利用一些语言机制和系统方法,设计一套防护系统,使之能够有效的降低 APP 的崩溃率,那么不仅 APP 的稳定性得到了保障,而且最重要的是可以减少不必要的加班。
Objective-C 语言是一门动态语言,我们可以利用 Objective-C 语言的 Runtime 运行时机制,对需要 Hook 的类添加 Category(分类),在各个分类的 +(void)load; 中通过 Method Swizzling 拦截容易造成崩溃的系统方法,将系统原有方法与添加的防护方法的 selector(方法选择器) 与 IMP(函数实现指针)进行对调。然后在替换方法中添加防护操作,从而达到避免以及修复崩溃的目的。
先来讲解下 unrecognized selector sent to instance(找不到对象方法的实现) 和 unrecognized selector sent to class(找不到类方法实现) 造成的崩溃问题。
1.Method Swizzling 方法的封装
由于这几种常见 Crash 的防护都需要用到 Method Swizzling 技术。所以我们可以为 NSObject 新建一个分类,将 Method Swizzling 相关的方法封装起来。
/********************* NSObject+MethodSwizzling.h 文件 *********************/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (MethodSwizzling)
/** 交换两个类方法的实现
* @param originalSelector 原始方法的 SEL
* @param swizzledSelector 交换方法的 SEL
* @param targetClass 类
*/
+ (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;
/** 交换两个对象方法的实现
* @param originalSelector 原始方法的 SEL
* @param swizzledSelector 交换方法的 SEL
* @param targetClass 类
*/
+ (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;
@end
/********************* NSObject+MethodSwizzling.m 文件 *********************/
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>
@implementation NSObject (MethodSwizzling)
// 交换两个类方法的实现
+ (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
swizzlingClassMethod(targetClass, originalSelector, swizzledSelector);
}
// 交换两个对象方法的实现
+ (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
swizzlingInstanceMethod(targetClass, originalSelector, swizzledSelector);
}
// 交换两个类方法的实现 C 函数
void swizzlingClassMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getClassMethod(class, originalSelector);
Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
// 交换两个对象方法的实现 C 函数
void swizzlingInstanceMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
2.Unrecognized Selector 防护
2.1 unrecognized selector sent to instance(找不到对象方法的实现)
如果被调用的对象方法没有实现,那么程序在运行中调用该方法时,就会因为找不到对应的方法实现,从而导致 APP 崩溃。比如下面这样的代码:
UIButton *testButton = [[UIButton alloc] init];
[testButton performSelector:@selector(someMethod:)];
testButton 是一个 UIButton 对象,而 UIButton 类中并没有实现 someMethod: 方法。所以向 testButoon 对象发送
someMethod: 方法,就会导致 testButoon 对象无法找到对应的方法实现,最终导致 APP 的崩溃。
那么有办法解决这类因为找不到方法的实现而导致程序崩溃的方法吗?
我们从『 ios 开发:『Runtime』详解(一)基础知识』知道了消息转发机制中三大步骤:消息动态解析、消息接受者重定向、消息重定向。通过这三大步骤,可以让我们在程序找不到调用方法崩溃之前,拦截方法调用。
大致流程如下:
- 1.消息动态解析:Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。我们可以通过重写这两个方法,添加其他函数实现,并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。若返回 NO 或者没有添加其他函数实现,则进入下一步。
- 2.消息接受者重定向:如果当前对象实现了 forwardingTargetForSelector:,Runtime 就会调用这个方法,允许我们将消息的接受者转发给其他对象。如果这一步方法返回 nil,则进入下一步。
- 3.消息重定向:Runtime 系统利用 methodSignatureForSelector: 方法获取函数的参数和返回值类型。
- 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
- 如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。
这里我们选择第二步(消息接受者重定向)来进行拦截。因为 -forwardingTargetForSelector 方法可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写。
具体步骤如下:
- 给 NSObject 添加一个分类,在分类中实现一个自定义的 -ysc_forwardingTargetForSelector: 方法;
- 利用 Method Swizzling 将 -forwardingTargetForSelector: 和 -ysc_forwardingTargetForSelector: 进行方法交换。
为什么不直接使用类别中的方法覆盖主类的forwardingTargetForSelector呢?
由于直接使用父类的覆盖掉会会警告, 说类别中实现的方法也被主类实现了, 而我们不想影响到主类的调用, 所以在做完自己的处理后,在调用主类的forwardingTargetForSelector
- 在自定义的方法中,先判断当前对象是否已经实现了消息接受者重定向和消息重定向。如果都没有实现,就动态创建一个目标类,给目标类动态添加一个方法。这里其实没必要动态创建目标类,使用正常的方式创建类就行, 这个类可以添加那些不存在的方法, 同时收集错误信息.
- 把消息转发给动态生成类的实例对象,由目标类动态创建的方法实现,这样 APP 就不会崩溃了。
实现代码如下:
#import "NSObject+SelectorDefender.h"
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>
@implementation NSObject (SelectorDefender)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 拦截 `-forwardingTargetForSelector:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(ysc_forwardingTargetForSelector:)
withClass:[NSObject class]];
});
}
// 自定义实现 `-ysc_forwardingTargetForSelector:` 方法
- (id)ysc_forwardingTargetForSelector:(SEL)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);
NSLog(@"出问题的类,出问题的对象方法 == %@ %@", errClassName, errSel);
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)Crash, "@@:@");
}
// 把消息转发到当前动态生成类的实例对象上
return [[cls alloc] init];
}
}
return [self ysc_forwardingTargetForSelector:aSelector];
}
// 动态添加的方法实现
static int Crash(id slf, SEL selector) {
return 0;
}
@end
2.2 unrecognized selector sent to class(找不到类方法实现)
同对象方法一样,如果被调用的类方法没有实现,那么同样也会导致 APP 崩溃。
例如,有这样一个类,声明了一个 + (id)aClassFunc; 的类方法, 但是并没有实现,就像下边的 YSCObject 这样。
/********************* YSCObject.h 文件 *********************/
#import <Foundation/Foundation.h>
@interface YSCObject : NSObject
+ (id)aClassFunc;
@end
/********************* YSCObject.m 文件 *********************/
#import "YSCObject.h"
@implementation YSCObject
@end
如果我们直接调用 [YSCObject aClassFunc]; 就会导致崩溃。
找不到类方法实现的解决方法和之前类似,我们可以利用 Method Swizzling 将 +forwardingTargetForSelector: 和 +ysc_forwardingTargetForSelector: 进行方法交换。
#import "NSObject+SelectorDefender.h"
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>
@implementation NSObject (SelectorDefender)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 拦截 `+forwardingTargetForSelector:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(ysc_forwardingTargetForSelector:)
withClass:[NSObject class]];
});
}
// 自定义实现 `+ysc_forwardingTargetForSelector:` 方法
+ (id)ysc_forwardingTargetForSelector:(SEL)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);
NSLog(@"出问题的类,出问题的类方法 == %@ %@", errClassName, errSel);
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)Crash, "@@:@");
}
// 把消息转发到当前动态生成类的实例对象上
return [[cls alloc] init];
}
}
return [self ysc_forwardingTargetForSelector:aSelector];
}
// 动态添加的方法实现
static int Crash(id slf, SEL selector) {
return 0;
}
@end
将 2.1 和 2.2 结合起来就可以拦截所有未实现的类方法和对象方法了。
3. KVC Crash
KVC Crash的常见原因
KVC(Key Value Coding),即键值编码,提供一种机制来间接访问对象的属性。而不是通过调用 Setter 、 Getter 方法进行访问。
KVC 日常使用造成崩溃的原因通常有以下几个:
1. key 不是对象的属性,造成崩溃。
2. keyPath 不正确,造成崩溃。
3. key 为 nil,造成崩溃。
4. value 为 nil,为非对象设值,造成崩溃。
常见的使用 KVC 造成崩溃代码:
/********************* KVCCrashObject.h 文件 *********************/
#import <Foundation/Foundation.h>
@interface KVCCrashObject : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
/********************* KVCCrashObject.m 文件 *********************/
#import "KVCCrashObject.h"
@implementation KVCCrashObject
@end
/********************* ViewController.m 文件 *********************/
#import "ViewController.h"
#import "KVCCrashObject.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 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.;
KVCCrashObject *objc = [[KVCCrashObject 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.
KVCCrashObject *objc = [[KVCCrashObject 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 会提示警告,传空变量则不会提示警告
KVCCrashObject *objc = [[KVCCrashObject 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 会崩溃
KVCCrashObject *objc = [[KVCCrashObject alloc] init];
[objc setValue:nil forKey:@"age"];
}
@end
KVC的setter和getter方法 ,KVC的查找过程
Setter方法
系统在执行 **setValue:forKey: **方法时,会把 key 和 value 作为输入参数,并尝试在接收调用对象的内部,给属性 key 设置 value 值。
通过以下几个步骤:
- 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: **方法,并引发崩溃。
KVCCrashObject *objc = [[KVCCrashObject alloc] init];
[objc setValue:@"value" forKey:@"name"];
Getter方法
系统在执行**valueForKey: **方法时,会将给定的 key 作为输入参数,在调用对象的内部进行以下几个步骤:
- 按顺序查找名为get<Key> 、<key> 、is<Key>、_<key> 的访问方法。如果找到,调用该方法,并继续执行步骤 5。否则继续向下执行步骤 2。
- 搜索形如countOf<Key> 、objectIn<Key>AtIndex: 、<key>AtIndexes: 的方法。
- 如果实现了countOf<Key> 方法,并且实现了objectIn<Key>AtIndex: 和 <key>AtIndexes: 这两个方法的任意一个方法,系统就会以 NSArray 为父类,动态生成一个类型为 NSKeyValueArray 的集合类对象,并调用上边的实现方法,将结果直接返回。
- 如果对象还实现了形如get<Key>:range: 的方法,系统也会在必要的时候自动调用。
- 如果上述操作不成功则继续向下执行步骤 3。
- 如果上边两步失败,系统就会查找形如countOf<Key> 、 enumeratorOf<Key> 、memberOf<Key>: 的方法。系统会自动生成一个 NSSet 类型的集合类对象,该对象响应所有 NSSet 方法并将结果返回。如果查找失败,则执行步骤 4。
- 如果上边三步失败,系统就会访问类的accessInstanceVariablesDirectly 方法。
- 如果返回 YES ,就按顺序查找名为 _<key> 、 _is<Key> 、 <key> 、 is<Key> 的实例变量。如果找到了对应的实例变量,则直接获取实例变量的值。并继续执行步骤 5。
- 如果返回 NO ,或者未找到对应的实例变量,则继续执行步骤 6。
- 分为三种情况:
- 如果检索到的属性值是对象指针,则直接返回结果。
- 如果检索到的属性值是 NSNumber 支持的基础数据类型,则将其存储在 NSNumber 实例中并返回该值。
- 如果检索到的属性值是 NSNumber 不支持的数据类型,则转换为 NSValue 对象并返回该对象。
- 如果一切都失败了,调用 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: 和 ysc_setValue:forKey: 进行方法交换。然后在自定义的方法中,添加对 key 为 nil 这种类型的判断。
还有最后一种 4. value 为 nil,为非对象设值,造成崩溃 的情况。
在 NSKeyValueCoding.h 文件中,有一个 setNilValueForKey: 方法。上边的官方注释给了我们答案。
在调用 setValue:forKey: 方法时,系统如果查找到名为 set<Key>: 方法的时候,会去检测 value 的参数类型,如果参数类型为 NSNmber 的标量类型或者是 NSValue 的结构类型,但是 value 为 nil 时,会自动调用 setNilValueForKey: 方法。这个方法的默认实现会引发崩溃。
所以为了防止这种情况导致的崩溃,我们可以通过重写 setNilValueForKey: 来解决。
至此,上文提到的 KVC 使用不当造成的四种类型崩溃就都解决了。下面我们来看下具体实现代码。
/********************* NSObject+KVCDefender.h 文件 *********************/
#import <Foundation/Foundation.h>
@interface NSObject (KVCDefender)
@end
/********************* NSObject+KVCDefender.m 文件 *********************/
#import "NSObject+KVCDefender.h"
#import "NSObject+MethodSwizzling.h"
@implementation NSObject (KVCDefender)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 拦截 `setValue:forKey:` 方法,替换自定义实现
[NSObject yscDefenderSwizzlingInstanceMethod:@selector(setValue:forKey:)
withMethod:@selector(ysc_setValue:forKey:)
withClass:[NSObject class]];
});
}
- (void)ysc_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];
}
- (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);
}
- (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;
}
@end
以上是关于iOS-unrecognized selector crash防护的主要内容,如果未能解决你的问题,请参考以下文章
selector.select()和selector.selectedKeys()
[[NSNotificationCenter defaultCenter]addObserver: selector:@selector(mymethod) name: object:];在 IOS