本文主要讲解消息转发,需要对 Class 的结构 selector,IMP,元类等概念有一点的了解,如果之前没了解,最好先暂停去了解一下
我们在 iOS 开发过程中应该都有碰到过这样的错:unrecognized selector sent to instance **
,是因为我们调用了一个不存在的方法,用 OC 消息机制来说,消息的接收者的 methods 列表中找不到 selector 对应的方法实现,这样就启动消息转发机制。但是 Objective-C 提供了3次机会让我们去补救 动态添加方法实现,转发方法 和 完整的消息转发
首先写一个 unrecognized selector sent to instance **
例子,然后通过这三种方法分别一一解决:
// Person.h
@interface Person : NSObject
- (void)run;
@end
// Person.m
@implementation Person
@end
// main.m
int main(int argc, const char * argv[]) {
Person *p = [[Person alloc] init];
[p run];
return 0;
}
动态添加方法实现
对象在收到 unrecognized selector sent to instance **
错误的时候,首先会调用 + (BOOL)resolveInstanceMethod:(SEL)sel
或则 + (BOOL)resolveClassMethod:(SEL)sel
询问是否有动态添加的方法来处理异常
void dynamicRun(id self, SEL _cmd)
{
NSLog(@"这是c语言的run函数 -- %@", self);
}
void dynamicBattle(id self, SEL _cmd)
{
NSLog(@"这是c语言的battle函数 -- %@", self);
}
- (void)wd_run {
NSLog(@"running");
}
+ (void)wd_battle {
NSLog(@"Battle");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel_isEqual(sel, NSSelectorFromString(@"run"))) {
// 添加OC方法
Method method = class_getInstanceMethod(self, @selector(wd_run));
IMP imp = method_getImplementation(method);
class_addMethod([self class], sel, imp, method_getTypeEncoding(method));
// 添加c函数
class_addMethod([self class], sel, (IMP)dynamicRun, "v@:");
return YES;
}
return NO;
}
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel_isEqual(sel, NSSelectorFromString(@"battle"))) {
// 获取元类
Class metaClass = object_getClass(self);
// 添加OC方法
Method method = class_getClassMethod(self, @selector(wd_battle));
IMP imp = method_getImplementation(method);
class_addMethod(metaClass, sel, imp, method_getTypeEncoding(method));
// 添加c函数
class_addMethod(metaClass, sel, (IMP)dynamicBattle, "v@:");
return YES;
}
return NO;
}
当 Person 收到 未知选择子 run
的时候,如果是实例方法,则会调用上文的 resolveInstanceMethod:
方法;如果是类方法,则会调用 resolveClassMethod:
方法。在方法内部,我们可以通过 class_addMethod
方法动态添加一个 run
的实现方法来解决,并返回 YES,如果这一步不解决,则返回 NO,然后会进入第二步的 转发
这里最值得注意的是,
resolveClassMethod
内部通过class_addMethod
的时候是添加到 Person 的元类上的
转发
如果在第一步的动态添加方法也没解决的选择子,会进入到这一步中,尝试转发给其他对象或类处理:
// Dog.h
@interface Dog : NSObject
- (void)run;
@end
// Dog.m
@implementation Dog
- (void)run {
NSLog(@"Dog running");
}
@end
// Person.m
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (sel_isEqual(aSelector, @selector(run))) {
return [[Dog alloc] init];
}
}
可能有些人有疑问了,为什么只有 forwardingTargetForSelector:
只有实例方法,没有对应的类方法呢?Objective-C 中的方法调用都是消息发送,并不没有明确表示调用的是实例方法还是类方法,只和接收者有关。也就是说消息消息接收者是实例对象,那么调用的就是实例方法;如果消息接收者是类对象,那么调用的就是类方法。下面可以试试转发类方法:
// Dog.h
+ (void)battle;
// Dog.m
+ (void)battle {
NSLog(@"Dog Battle");
}
// Person.m
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (sel_isEqual(aSelector, @selector(battle))) {
return [Dog class];
}
return [super forwardingTargetForSelector:aSelector];
}
没报错,控制台也成功输出了
Dog Battle
完整的消息转发
如果第二步的 forwardingTargetForSelector
方法返回 nil,那么就会进入消息转发的最后阶段——完整的消息转发,这一步骤比较麻烦,需要实现2个方法: methodSignatureForSelector:
返回未知选择子 run
的签名; forwardInvocation:
拿到对应的信息进行处理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (sel_isEqual(aSelector, @selector(run))) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (sel_isEqual(anInvocation.selector, @selector(run))) {
Dog *d = [[Dog alloc] init];
[anInvocation invokeWithTarget:d];
}
}
这时控制台会打印
Dog running
对应的类方法的完整消息转发,和第二步一样的,只需要将实例方法 -
改成类方法 +
就行了
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (sel_isEqual(aSelector, @selector(battle))) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
if (sel_isEqual(anInvocation.selector, @selector(battle))) {
[anInvocation invokeWithTarget:[Dog class]];
}
}
如果这三个步骤都没实现的话,那么还是会调用 - (void)doesNotRecognizeSelector:(SEL)aSelector
这个方法报错。当然,线上环境肯定是不能出现此类问题的
最后附上用 Sketch 画的流程图:
其实之前有写过消息转发的文章,但是表达有点问题,就拖到了现在😓