一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情。
如何选择拦截方案的建议
对于以上的三个步骤,我们该选择哪一个步骤来进行拦截呢?
-
动态方法解析 - 不建议
- 这个方法会为类添加本身不存在的方法,绝大多数情况下,这个方法时冗余的。
respondsToSelector:和instancesRespondToSelector:这两个方法都会调用到resolveInstanceMethod:,那么在我们需要使用这两个方法进行判断的时候,就会出现我们不想看到的情况。
-
消息快速转发 - 推荐
会拦截掉已经通过消息常规转发实现的消息转发,但是可以通过判断避开对NSObject子类的消息常规转发的拦截。 -
消息常规转发 - 推荐
这一步不会对原有的消息转发机制产生影响,缺点是更大的性能开销。
快速转发拦截方案
我们可以创建一个例如:crashPreventor的类,在forwardingTargetForSelector:中为crashPreventor添加selector,最后返回crashPreventor的实例。从而让crashPreventor的实例响应这个selector。具体代码如下:
@implementation NSObject (SelectorPreventor)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (id)forwardingTargetForSelector:(SEL)aSelector{
Class rootClass = NSObject.class;
Class currentClass = self.class;
return [self.class actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
}
+ (id)forwardingTargetForSelector:(SEL)aSelector {
Class rootClass = objc_getMetaClass(class_getName(NSObject.class));
Class currentClass = objc_getMetaClass(class_getName(self.class));
return [self actionForwardingTargetForSelector:aSelector rootClass:rootClass currentClass:currentClass];
}
+ (id)actionForwardingTargetForSelector:(SEL)aSelector rootClass:(Class)rootClass currentClass:(Class)currentClass {
// 过滤掉内部对象
NSString *className = NSStringFromClass(currentClass);
if ([className hasPrefix:@"_"]) {
return nil;
}
SEL methodSignatureSelector = @selector(methodSignatureForSelector:);
IMP rootMethodSignatureMethod = class_getMethodImplementation(rootClass, methodSignatureSelector);
IMP currentMethodSignatureMethod = class_getMethodImplementation(currentClass, methodSignatureSelector);
if (rootMethodSignatureMethod != currentMethodSignatureMethod) {
return nil;
}
NSString * selectorName = NSStringFromSelector(aSelector);
// 上报异常
// unrecognized selector sent to class XXX
// unrecognized selector sent to instance XXX
NSLog(@"unrecognized selector crash:%@:%@", className, selectorName);
// 创建crashPreventor类
NSString *targetClassName = @"crashPreventor";
Class cls = NSClassFromString(targetClassName);
if (!cls) {
// 如果要注册类,则必须要先判断class是否已经存在,否则会产生崩溃
// 如果不注册类,则可以重复创建class
cls = objc_allocateClassPair(NSObject.class, targetClassName.UTF8String, 0);
objc_registerClassPair(cls);
}
// 如果类没有对应的方法,则动态添加一个
if (!class_getInstanceMethod(cls, aSelector)) {
Method method = class_getInstanceMethod(currentClass, @selector(crashPreventor));
class_addMethod(cls, aSelector, method_getImplementation(method), method_getTypeEncoding(method));
}
return [cls new];
}
#pragma clang diagnostic pop
- (id)crashPreventor {
return nil;
}
@end
这里有几个点需要提一下:
- (id)forwardingTargetForSelector:(SEL)aSelector;和+ (id)forwardingTargetForSelector:(SEL)aSelector;都要在NSObject的分类中重写。前者对应实例方法,后者对应类方法。- 过滤掉一些系统内部对象,否则在启动的时候就会有一些奇怪的异常被捕获到。
- 我们需要判断当前类是否实现了
methodSignatureForSelector:方法,如果实现了该方法,就认为当前类已经实现了自己的消息转发机制,我们不对其进行拦截。 - 细心的我们肯定有注意到,不管是类方法还是实例方法,我们都是向
crashPreventor中添加实例方法。这是因为,我们的响应对象时crashPreventor实例,而selector不区分实例方法还是类方法。我们这么处理最终对方法执行来说不会有什么差别。
常规转发拦截方案
实现比较简单,我们直接上代码:
@implementation NSObject (SelectorPreventor)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation------");
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"@"];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation------");
}
#pragma clang diagnostic pop
@end
同样的,类方法和实例方法我们都需要重写。
在methodSignatureForSelector:中我们返回一个返回值为void的NSMethodSignature,在forwardInvocation:中我们不做任何事情。这样将性能消耗减到最小。
以上:我们可以选择其中一种方式来实现我们对unrecognized selector的拦截,跟unrecognized selector彻底说拜拜啦(手动微笑)。