消息转发的performSelector和invocation

消息转发实际上是调用了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"

    1. 备援接收者
- (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来使用的话,就是一种所谓的黑魔法吧.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,857评论 6 13
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,129评论 0 9
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 何人红楼中耳鬓厮磨, 何人西游中斩妖除魔; 何人三国间拜相称侯, 何人水浒边落草为寇。 是谁,乐不思蜀醉了红楼; ...
    汴梁公子鱼阅读 333评论 1 1
  • 好像,是你的声音 却又分明不是 许是恍惚,许是后悔 没来由的一条短信 将我刚酝酿的记忆扼杀 你说: 我从未真正开过...
    梦里梦外谛听你阅读 202评论 1 5