消息转发实际上是调用了C底层的函数
我们都知道在OC中方法,使用方法叫做发送消息,其实这种说法主要是因为OC在调用方法的时候会将一个方法转化为
void objc_msgSend(id self, SEL cmd, ...)
该第一个参数是对象,第二个参数是方法名字,第三个参数是方法参数.
这个时候,如果给对象发送一个当前对象不存在的方法,系统暂时还不会崩溃,它还会调用三个方法给开发者上次机会补救这个方法的"缺失"(事实上是因此OC才能成为一门真正动态的语言)
给函数发送失败后会发生什么事情
- 1.动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel;//实例方法
+ (BOOL)resolveClassMethod : (SEL)selector//类方法
首先,当接受到未能识别的选择子时,运行时系统会调用该函数用以给对象一次机会来添加相应的方法实现,如果用户在该函数中动态添加了相应方法的实现,则跳转到方法的实现部分,并将该实现存入缓存中,以供下次调用。(52个建议中写道,其实这个缓存机制会缓存在"快速哈希表"中,下次获取的时候就能首先在这个方法列表中寻找高频使用的方法,减少遍历次数)
首先我们先定义一个Person类
Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic ,copy ) NSString* name;
@property (nonatomic , assign ) NSUInteger age;
-(void)run:(NSString*)name withWhere:(NSString*)where;
-(void)eat;
-(void)dynamicSelector;
@end
----------------------------------------------
Person.m
@implementation Person
-(void)run:(NSString*)name withWhere:(NSString*)where{
NSLog(@"%@ is running in %@ ",name,where);
}
-(void)eat{
NSLog(@"eat");
}
-(void)dynamicSelector{
NSLog(@"dynamicSelector");
}
@end
好吧,很简单的一个类,主要有一个run/eat/dynamicSelector方法.没有其他好介绍的了,接下来我们在控制器中实例化一个对象,然后调用某个方法
Person * p = [[Person alloc]init];
self.person = p;
#pragma clang diagnostic push//添加该代码可以去除编译器带来的警告,好烦人啊,后面就不再写这句了,默认在找不到方法的地方都应该添加这几行代码
#pragma clang diagnostic ignored "-Wundeclared-selector"
[self performSelector:@selector(dynamicSelector) withObject:nil];
#pragma clang diagnostic pop
因为self里面肯定没有dynamicSelector啊,我们想要在这里调用Person中的dynamicSelector要怎么办呢?
这里就可以使用我们动态方法解析机制了,系统在该类中找不到这个方法的时候就会调用下面这个函数,执行里面的方法:
//定义一个备用的C方法来防止程序崩溃
void myMehtod(id self,SEL _cmd){
NSLog(@"This is added dynamic");
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(dynamicSelector)) {
class_addMethod([self class], sel, (IMP)myMehtod, "v@:");
return YES;
} else {
return [super resolveInstanceMethod:sel];
}
}
此处被调用后,控制台打印了"This is added dynamic"
- 备援接收者
- (id)forwardingTargetForSelector:(SEL)aSelector;
如果运行时在消息转发的第一步中未找到所调用方法的实现,那么当前接收者还有第二次机会进行未知选择子的处理。这时运行期系统会调用上述方法,并将未知选择子作为参数传入,该方法可以返回一个能处理该选择子的对象,运行时系统会根据返回的对象进行查找,若找到则跳转到相应方法的实现,则消息转发结束。
-(id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector==@selector(dynamicSelector)&&[self.person respondsToSelector:@selector(dynamicSelector)]) {
return self.person;
} else {
return [super forwardingTargetForSelector:aSelector];
}
}
这里的调用频率还是可以很高,因为很多时候此方法的调用还是很廉价的,经过第一步的方法查找如果失败,直接使用这个方法继续查找,这个时候你可以安排这个对象来实现这个方法.如Person中有能够响应dynamicSelector的方法,则返回这个self.Person来执行这个方法,此处打印台中会打印:dynamicSelector
- 3.完整的消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation;
当运行时系统检测到第二步中用户未返回能处理相应选择子的对象时,那么来到这一步就要启动完整的消息转发机制了。该方法可以改变消息调用目标,运行时系统根据所改变的调用目标,向调用目标方法列表中查询对应方法的实现并实现跳转,这种方式和第二步的操作非常相似。当然你也可以修改方法的选择子,亦或者向所调用方法中追加一个参数等来跳转到相关方法的实现。
-(void)forwardInvocation:(NSInvocation *)anInvocation{
if ([self.person respondsToSelector:[anInvocation selector]]) {
[anInvocation invokeWithTarget:self.person];
} else {
[super forwardInvocation:anInvocation];
}
}
此处的代码也会实现Person中的dynamicSelector,这个时候有的同学就懵逼了(反正我是懵逼了),这个NSInvocation到底是什么,怎么这么神奇...
NSInvocation方法调用解析
标题中写了performSelector和invocation,实际上的更像说说后者,前者由于所带的参数有局限性,所以没有办法,最多只能whitObject*2 ,参数个数一旦超过两个的时候,我们就得想其他办法来布局了.那么,我们首先考虑到的方案就是NSiNInvocation啦.
- 1.要使用该方法,第一步是要进行方法"签名":
//根据方法来初始化NSMethodSignature
NSMethodSignature* signature = [Person instanceMethodSignatureForSelector:@selector(run:withWhere:)];
方法签名中保存了方法的名称/参数/返回值,协同NSInvocation来进行消息的转发.方法签名一般是用来设置参数和获取返回值的, 和方法的调用没有太大的关系
但有一点要注意:如果方法不存在的话就会崩溃,所以我们在签名以后还要做一件事情:方法不存在的时候抛出异常提示:
if (signature == nil) {
NSString *info = [NSString stringWithFormat:@"方法找不到"];
[NSException raise:@"方法调用出现异常" format:info, nil];
// NSLog(@"%@",signature);
}
- 2.创建NSInvocation对象
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:signature];
//设置方法调用者
invocation.target = self.person;
//注意:这里的方法名一定要与方法签名类中的方法一致
invocation.selector = @selector(run:withWhere:);
//参数
NSString* name = @"小寒";
NSString* where = @"广州";
//参数数组
NSMutableArray* objects = [NSMutableArray array];
[objects addObject:name];
[objects addObject:where];
这里面的objects数组可以装多个参数,到时会按顺序塞进去参数列表中,供方法调用,下面开始使用下面方法设置参数:
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
方法中的第二个参数就是用来存放插入的方法参数的,这里按道理来说,我们需要使用遍历objects数组的个数来赋值参数,但是由于外界传进来的参数个数是不可控的,此处不能通过遍历参数数组来设置参数.还记得signature做了方法签名吗?其实最开始的时候有提到过,签名后能获得方法的名称/参数/返回值,其中里面还提供了通过numberOfArguments方法获取的参数个数(但是这个方面里面包含了包含self和_cmd的,所以我们要比较方法需要的参数和外界传进来的参数个数,并且取它们之间的最小值)
NSUInteger argsCount = signature.numberOfArguments - 2;
NSUInteger arrCount = objects.count;
//获取最小值
NSUInteger count = MIN(argsCount, arrCount);
NSLog(@"argsCount: %ld,arrCount: %ld,count: %ld",argsCount,arrCount,count);
for (int i = 0; i<count; i++) {
id arg = objects[i];
if ([arg isKindOfClass:[NSNull class]]) arg = nil;
//这里的Index要从2开始,以为0跟1已经被占据了,分别是self(target),selector(_cmd),即使方法的参数为空的时候,此处也应该加2
[invocation setArgument:&arg atIndex: i + 2 ];
}
- 方法的调用和返回值
需要调用该方法,实际上就是调用invoke方法
[invocation invoke];
执行完这一步以后,就已经可以顺利调用self.person方法中的run:withWhere:方法.
如果你的代码中需要有返回值的话,可以调用下面这个方法:
//此处可以获得参数的返回值
id res = nil;
if (signature.methodReturnLength != 0) {
[invocation getReturnValue:&res];
}
NSLog(@"return : %@",res);
现在可以感受到oc中消息调用的魅力了吧,强大之处实在令人佩服,结合runtime来使用的话,就是一种所谓的黑魔法吧.