使用 NSInvocation 向对象发送消息

1. Objective-C 的消息派发

Objective-C 是动态语言,所有的消息都是在 Runtime 进行派发的

1.1. objc_msgSend

�最底层的转发函数为objc_msgSend,它的定义如下

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

从以上的定义我们可以得出一个消息转发包含了几大要素:target、selector、arguments、return value,objc_msgSend 是 C 函数,苹果不提倡我们直接使用该函数来向对象消息。

1.2. performSelector

想必大家都知道使用 performSelector 给对象发送消息,但是其有几个短板

  • 在 ARC 场景下 performSelector 可能会造成内存泄漏
  • performSelector 至多接收 2 个参数,如果参数多余 2 个,我们就无法使用 performSelector 来向对象发送消息了。
  • performSelector 限制参数类型为 id,以标量数据(int double NSInteger 等)为参数的方法使用 performSelector 调用会出现各种各样诡异的问题

1.3. NSInvocation

NSInvocation 是苹果工程师们提供的一个高层的消息转发系统。它是一个命令对象,可以给任何 Objective-C 对象类型发送消息,接下来将介绍 NSInvocation 的�用法。

2. NSInvocation 的使用

2.1. 初始化

必须使用工厂方法 invocationWithMethodSignature: 来创建一个 NSInvocation 实例。工厂方法的参数是一个 NSMethodSignature 对象。一般使用 NSObject 的实例方法 methodSignatureForSelector: 或者类方法 instanceMethodSignatureForSelector: 来创建对应 selector 的 NSMethodSignature 对象。

例:创建类方法的签名与实例方法签名

- (void)createClassMethodSignature:(SEL)selector {
    NSMethodSignature *methodSignature = [[self class] methodSignatureForSelector:selector];
}

- (void)createInstanceMethodSignature:(SEL)selector {
    NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector];
}

2.2. 接受对象以及选择子

需要注意的是 NSMethodSignature 对象仅仅表示了方法的签名:方法的请求、返回数据的编码。所以在使用 NSMethodSignature 来创建 NSInvocation 对象之后仍需指定消息的接收对象和选择子

NSMethodSignature *methodSignature = [[self class] methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:[self class]];
[invocation setSelector:selector];

原则是接收对象的对应选择子需要跟 NSMethodSignature 相匹配。但是根据实践来说,只要不造成 NSInvocation setArgument:atIndex 越界的异常,都是可以成功转发消息的,并且转发成功之后,未赋值的参数都将被赋值为 nil。

例如:

- (void)greetingWithInvocation {
    NSMethodSignature *methodSignature = [self methodSignatureForSelector:@selector(greetingWithName:)];
    
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    [invocation setSelector:@selector(greetingWithAge:name:)];
    
//    NSString *name = @"Tom";
//    [invocation setArgument:&name atIndex:3];
    NSUInteger age = 10;
    [invocation setArgument:&age atIndex:2];
    
    [invocation invokeWithTarget:self];
}

- (void)greetingWithName:(NSString *)name {
    NSLog(@"Hello World %@!",name);
}

- (void)greetingWithAge:(NSUInteger)age name:(NSString *)name {
    NSLog(@"Hello %@ %ld!", name, (long)age);
}

执行结果:

2017-05-03 16:16:29.815 NSInvocationDemo[50214:49610519] Hello (null) 10!

2.3. 参数传递

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

以上为 NSInvocation 类中定义针对参数的操作。 argumentLocation 参数为 void * 类型,表示需要传递指针地址给它。idx 参数是从 2 开始的,0 和 1 分别代表 target 和 selector,虽然可以�直接使用 getArgument:atIndex 来获取 target 和 selector,但是不如 NSInvocation 的 target 以及 selector 属性来的方便。需要注意的是当 idx 超过对应 NSMethodSignature 的参数个数的时候获取参数和设置参数的方法都会抛出 NSInvalidArgumentException 异常。

例如:给 greetingWithName: 方法传参

- (void)sendMsgWithInvocation {
    NSString *name = @"Tom";
    SEL selector = @selector(greetingWithName:);

    NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    [invocation setTarget:self];
    [invocation setSelector:selector];
    [invocation setArgument:&name atIndex:2];
    [invocation invoke];
}

- (void)greetingWithName:(NSString *)name {
    NSLog(@"Hello %@!", name);
}

需要特别注意 setArgument:atIndex: 默认不会强引用它的 argument,如果 argument 在 NSInvocation 执行的时候之前被释放就会造成野指针异常(EXC_BAD_ACCESS)。

NSInvocation_Crash.png

如上图所示, invocation 未�强引用它的 target,在控制器弹出之后,target �被释放,然后再 invoke 这个 invocation 会造成野指针异常。调用 retainArguments 方法来强引用参数(包括 target 以及 selector)

2.4. 返回数据

NSInvocation 类中的返回数据的方法如下

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

可以看到返回数据仍然是通过传入指针来进传值的。例:

- (void)plusWithInvocation {
    NSMethodSignature *methodSignature = [self methodSignatureForSelector:@selector(plusWithA:B:)];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    [invocation retainArguments];
    [invocation setTarget:self];
    [invocation setSelector:@selector(plusWithA:B:)];
    
    int a = 10;
    [invocation setArgument:&a atIndex:2];
    int b = 5;
    [invocation setArgument:&b atIndex:3];
    
    [invocation invoke];
    
    int result;
    [invocation getReturnValue:&result];
    NSLog(@"%ld", (long)result);
}

- (int)plusWithA:(int)a B:(int)b {
    return a + b;
}

输出结果为:

2017-05-03 17:13:31.884 NSInvocationDemo[50948:49713408] 15

需要注意的是:考虑到 getReturnValue 方法仅仅是将返回数据拷贝到提供的缓存区(retLoc)内,并不会考虑到此处的内存管理,所以如果返回数据是对象类型的,实际上获取到的返回数据是 __unsafe_unretained 类型的,上层函数再�把它作为返回数据返回的时候就会造成野指针异常。通常的解决方法有2种:

第一种:新建一个相同类型的对象并指向它,这样做 result 就会强引用 tempResult,当做返回数据返回之后会自动添加 autorelease 关键字,也就不会造成野指针异常。

NSNumber __unsafe_unretained *tempResult;
[invocation getReturnValue:&tempResult];
NSNumber *result = tempResult;
return result;

第二种:�使用 __bridge 将缓存区转换为 Objective-C 类型,这种做法其实跟第一种相似,但是我们更建议使用这种方式来解决以上问题,因为 getReturnValue �本来就是给缓存区写入数据,缓存区声明为 void* 类型更为合理,然后通过 __bridge 方式转换为 Objective-C 类型并�且将该内存区的内存管理交给 ARC。

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

推荐阅读更多精彩内容