Crash拦截器 - 让unrecognized selector消失
在本文中,我们将了解到如下内容:
- 基础的消息转发流程
- unrecognized selector 拦截建议
- 快速转发(Fast Forwarding)拦截unrecognized selector
- 常规转发(Normal Forwarding)拦截unrecognized selector
前言
我们在第一天学习Objective-C
这一门语言的时候,就被告知这是一门动态语言。C
这样的编译语言,在编译阶段就确定了所有函数的调用链,如果函数没有被实现,编译就根本不过了。而基于动态语言的特性,在编译期间,我们无法确认程序在运行时要调用哪一个函数,某一个未被实现的函数是否会在运行时被实现。
这样就可能会出现运行时发现调用的函数根本不存在的尴尬,这也就是我们收到unrecognized selector sent to XXX
这样的崩溃的原因了(动态语言也有让人心累的地方,手动叹气)。
这篇文章要讨论的就是如果遇到了这种尴尬情况的时候,我们该如何避免我们最最最讨厌的崩溃(是的,所有的崩溃都是最最最让人讨厌的)。
消息转发流程
我们知道在我们调用某一个方法之后,最终调用的是objc_msgSend()
这样一个方法,发送消息
(selector
)给消息接收者
(receiver
)。这个方法会根据OC
的消息发送机制在receiver
中查找selector
。如果没有查找到,就会出现上述的运行时调用了未实现的函数的尴尬局面了。
不过为了缓解这种尴尬,我们还有机会来挣扎。这挣扎机会就是消息转发流程。
消息转发流程包含以下3个步骤:
- 动态方法解析:
resolveInstanceMethod:
和resolveClassMethod:
- 消息转发
- 快速转发:
forwardingTargetForSelector:
- 常规转发:
methodSignatureForSelector:
和forwardInvocation:
- 快速转发:
消息转发流程是以动态方法解析、消息快速转发、消息常规转发这样的顺序来执行的。如果其中任意一个步骤能使消息被执行,那么就不会出现unrecognized selector sent to XXX
的崩溃。
动态方法解析
resolveInstanceMethod:
这个方法的作用是动态地为selector
提供一个实例方法的实现。而resolveClassMethod:
则是提供一个类方法的实现。
所以我们可以在这两个方法中,为对象添加方法的实现,再返回YES
告诉已经为selector
添加了实现。这样就会重新在对象上查找方法,找到我们新添加的方法后就直接调用。从而避免掉unrecognized selector sent to XXX
。
需要注意的是: 这两个方法会响应respondsToSelector:
和instancesRespondToSelector:
。
消息快速转发
forwardingTargetForSelector:
的作用是将消息转发给其它对象去处理。
我们可以在这个方法中,返回一个对象,让这个对象来响应消息。
需要注意的是: 如果在这个方法中返回self
或nil
,则表示没有可响应的目标。
消息常规转发
forwardInvocation:
的作用也是将消息转发给其它对象。不过与 消息快速转发 不同的是该方法需要手动的创建一个NSInvocation
对象,并手动地将新消息发送给新的接收者。
很显然,这种方式会比 消息快速转发 付出更大的消耗。
如何选择拦截方案的建议
对于以上的三个步骤,我们该选择哪一个步骤来进行拦截呢?
- 动态方法解析 - 不建议
- 这个方法会为类添加本身不存在的方法,绝大多数情况下,这个方法时冗余的。
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
彻底说拜拜啦(手动微笑)。
作者:一纸苍白
链接:https://www.jianshu.com/p/90b04882c595