为什么要谈消息转发呢,对我们又有什么用处呢?虽然平时开发过程中可能没有太关心,但是我们始终都在接触它,比如说开发过程中我们经常会碰到这样的报错
unrecognized selector sent to instance **
原因是我们调用了一个不存在的方法。其实就是说消息的接受者没有找到对应的selector,在这个时候就启用了消息转发机制,当然,我们可以通过在消息转发的过程中告诉对象如何处理这种情况。
分析之前我们来写个例子,创建一个mac的Command Line Tool程序,创建一个类Person,代码如下:
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
[p performSelector:NSSelectorFromString(@"eat")];
[p performSelector:NSSelectorFromString(@"walk")];
}
return 0;
}
#import
@interface Person : NSObject
- (void)eat;
@end
#import "Person.h"
@implementation Person
- (void)eat
{
NSLog(@"person eat");
}
@end
运行一下,可以想到的结果是
2018-10-26 15:55:47.747779+0800 MsgLine[78325:5469322] person eat
2018-10-26 15:55:47.748122+0800 MsgLine[78325:5469322] -[Person walk]: unrecognized selector sent to instance 0x1007089c0
接下来我们来分析一下,当对象收到无法解读的消息时,都会经过哪几个步骤来处理:
第一步
调用
+(BOOL)resolveInstanceMethod:(SEL)sel
或者
+ (BOOL)resolveClassMethod:(SEL)sel
询问是否添加方法进行处理,我们改下上面的代码
#import "Person.h"
@implementation Person
- (void)eat
{
NSLog(@"person eat");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"resolveInstanceMethod");
return [super resolveInstanceMethod:sel];
}
@end
运行结果如下
2018-10-26 15:57:22.804747+0800 MsgLine[78357:5474517] person eat
2018-10-26 15:57:22.805032+0800 MsgLine[78357:5474517] resolveInstanceMethod
2018-10-26 15:57:22.805104+0800 MsgLine[78357:5474517] -[Person walk]: unrecognized selector sent to instance 0x100708610
依然崩溃,但是已经进入了resolveInstanceMethod:
方法,我们再次改造
#import "Person.h"
#import
@implementation Person
- (void)eat
{
NSLog(@"person eat");
}
- (void)walk
{
NSLog(@"person walk");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"resolveInstanceMethod");
if (sel == @selector(walk)){
return class_addMethod([self class], sel, method_getImplementation(class_getInstanceMethod([self class], sel)), [@"V@:" UTF8String]);
}
return [super resolveInstanceMethod:sel];
}
@end
这次运行后就发现没有崩溃了,同时控制台输出正常
2018-10-26 16:12:23.353887+0800 MsgLine[78585:5512283] person eat
2018-10-26 16:12:23.354247+0800 MsgLine[78585:5512283] person walk
当Person收到了未知walk
消息的时候,如果是实例方法会首选调用上文的resolveInstanceMethod:
方>法,如果我们通过class_addMethod
方法动态添加了一个walk
的实现方法来解决掉这条未知的消息,时消息转发过程结束。
第二步【如果没有添加方法,返回了NO,那么才有第二步】
询问其他类是否能处理这个消息
- (id)forwardingTargetForSelector:(SEL)aSelector
这次我们进行如下改造
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
[p performSelector:NSSelectorFromString(@"eat")];
[p performSelector:NSSelectorFromString(@"walk")];
[p performSelector:NSSelectorFromString(@"fly")];
}
return 0;
}
创建一个新类Plane,同时添加- (void)fly
方法
#import
@interface Plane : NSObject
- (void)fly;
@end
#import "Plane.h"
@implementation Plane
- (void)fly
{
NSLog(@"Plane fly");
}
@end
main
函数里面调用了fly
方法,如果我们不什么都不做,最后仍然会抛出unrecognized selector sent to instance **异常
,这次我们不动态添加方法了,而是在- (id)forwardingTargetForSelector:(SEL)aSelector
里面做文章
#import "Person.h"
#import "Plane.h"
#import
@implementation Person
- (void)eat
{
NSLog(@"person eat");
}
- (void)walk
{
NSLog(@"person walk");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"resolveInstanceMethod");
if (sel == @selector(walk)){
return class_addMethod([self class], sel, method_getImplementation(class_getInstanceMethod([self class], sel)), [@"V@:" UTF8String]);
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"forwardingTargetForSelector");
Plane *plane = [[Plane alloc] init];
if ([plane respondsToSelector: aSelector]) {
return plane;
}
return [super forwardingTargetForSelector: aSelector];
}
@end
运行之后控制台输出如下
2018-10-26 16:51:30.386475+0800 MsgLine[79164:5590896] person eat
2018-10-26 16:51:30.386752+0800 MsgLine[79164:5590896] person walk
2018-10-26 16:51:30.386776+0800 MsgLine[79164:5590896] resolveInstanceMethod
2018-10-26 16:51:30.386811+0800 MsgLine[79164:5590896] forwardingTargetForSelector
2018-10-26 16:51:30.386843+0800 MsgLine[79164:5590896] Plane fly
这里Plane
能够处理这条消息,所以这条消息被plane
成功处理,消息转发流程结束,如果不能处理的话,那么才会进入下一步
第三步【如果没有交给其他类进行处理,就进入下一步】
获取方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
调用
- (void)forwardInvocation:(NSInvocation *)anInvocation
同样,我们修改我们的代码,修改main.m
为
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
[p performSelector:NSSelectorFromString(@"eat")];
[p performSelector:NSSelectorFromString(@"walk")];
[p performSelector:NSSelectorFromString(@"fly")];
[p performSelector:NSSelectorFromString(@"pluckStars")];
}
return 0;
}
添加一个新类Spaceship
,并实现pluckStars
方法
#import
@interface Spaceship : NSObject
- (void)pluckStars;
@end
#import "Spaceship.h"
@implementation Spaceship
- (void)pluckStars
{
NSLog(@"Spaceship pluckStars");
}
@end
然后,修改我们在Person
里面的代码
#import "Person.h"
#import "Plane.h"
#import "Spaceship.h"
#import
@implementation Person
- (void)eat
{
NSLog(@"person eat");
}
- (void)walk
{
NSLog(@"person walk");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"resolveInstanceMethod");
if (sel == @selector(walk)){
return class_addMethod([self class], sel, method_getImplementation(class_getInstanceMethod([self class], sel)), [@"V@:" UTF8String]);
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"forwardingTargetForSelector");
Plane *plane = [[Plane alloc] init];
if ([plane respondsToSelector: aSelector]) {
return plane;
}
return [super forwardingTargetForSelector: aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"methodSignatureForSelector");
if (aSelector == @selector(pluckStars)) {
return [NSMethodSignature signatureWithObjCTypes:"V@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"forwardInvocation");
if ([anInvocation selector] == @selector(pluckStars)) {
Spaceship *spaceship = [[Spaceship alloc] init];
[anInvocation invokeWithTarget:spaceship];
}else{
[super forwardInvocation:anInvocation];
}
}
@end
再次运行,结果如下:
2018-10-29 14:18:42.910585+0800 MsgLine[33073:7581724] person eat
2018-10-29 14:18:42.910858+0800 MsgLine[33073:7581724] person walk
2018-10-29 14:18:42.910888+0800 MsgLine[33073:7581724] resolveInstanceMethod
2018-10-29 14:18:42.910933+0800 MsgLine[33073:7581724] forwardingTargetForSelector
2018-10-29 14:18:42.910956+0800 MsgLine[33073:7581724] Plane fly
2018-10-29 14:18:42.910979+0800 MsgLine[33073:7581724] resolveInstanceMethod
2018-10-29 14:18:42.910996+0800 MsgLine[33073:7581724] forwardingTargetForSelector
2018-10-29 14:18:42.911016+0800 MsgLine[33073:7581724] methodSignatureForSelector
2018-10-29 14:18:42.911040+0800 MsgLine[33073:7581724] resolveInstanceMethod
2018-10-29 14:18:42.911106+0800 MsgLine[33073:7581724] forwardInvocation
2018-10-29 14:18:42.911156+0800 MsgLine[33073:7581724] Spaceship pluckStars
仔细看,当我们调用了[p performSelector:NSSelectorFromString(@"pluckStars")];
以后,正常的输出应该是
2018-10-29 14:18:42.910979+0800 MsgLine[33073:7581724] resolveInstanceMethod
2018-10-29 14:18:42.910996+0800 MsgLine[33073:7581724] forwardingTargetForSelector
2018-10-29 14:18:42.911016+0800 MsgLine[33073:7581724] methodSignatureForSelector
2018-10-29 14:18:42.911106+0800 MsgLine[33073:7581724] forwardInvocation
2018-10-29 14:18:42.911156+0800 MsgLine[33073:7581724] Spaceship pluckStars
那么为什么获取了方法签名以后本应该是forwardInvocation
的,为什么又多出来了一句
2018-10-29 14:18:42.911040+0800 MsgLine[33073:7581724] resolveInstanceMethod
我试着找了很多消息转发的文章,发现大家其实都有这个问题,但是到这一步直接忽略了这个问题,而官方文档其实也没针对这个问题有解释,除了逆向查看实现,我们还可以查看堆栈信息,首先我们在- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
函数中的返回值处return [NSMethodSignature signatureWithObjCTypes:"V@:@"];
下断点,运行程序,单步查看,发现到这里
0x7fff48a8d704 <+514>: callq 0x7fff48b9b99e ; symbol stub for: object_getClass
0x7fff48a8d709 <+519>: movq 0x57c918c0(%rip), %r13 ; "_forwardStackInvocation:"
0x7fff48a8d710 <+526>: movq %rax, %rdi
0x7fff48a8d713 <+529>: movq %r13, %rsi
0x7fff48a8d716 <+532>: callq 0x7fff48b9b4dc ; symbol stub for: class_respondsToSelector
其实如果你在+ (BOOL)resolveInstanceMethod:(SEL)sel
方法内打印sel
方法名的时候,你会发现多出来的forwardInvocation
其实就是多出了一个_forwardStackInvocation
,为什么多了这个呢,往下看发现
0x7fff48a8d716 <+532>: callq 0x7fff48b9b4dc ; symbol stub for: class_respondsToSelector
到了这里就明白了,在动态实现了查找后,系统会重载respondsToSelector
让对外接口查找动态实现函数的时候返回YES,以保证对外接口的行为统一。而respondsToSelector
又触发了+ (BOOL)resolveInstanceMethod:(SEL)sel
,所以也就多了一句resolveInstanceMethod
,但是如果你这样修改main.m
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
[p performSelector:NSSelectorFromString(@"eat")];
[p performSelector:NSSelectorFromString(@"walk")];
[p performSelector:NSSelectorFromString(@"fly")];
[p performSelector:NSSelectorFromString(@"pluckStars")];
[p performSelector:NSSelectorFromString(@"pluckStars")];
}
return 0;
}
那么可以肯定的是,第二次的[p performSelector:NSSelectorFromString(@"pluckStars")];
就不会再多打印一次resolveInstanceMethod
了,因为respondsToSelector
重载后,已经知道返回值是NO
了,也就不需要再次重载了,所以,上面的程序运行完以后,结果就是:
2018-10-29 15:42:23.653136+0800 MsgLine[34473:7749218] person eat
2018-10-29 15:42:23.653406+0800 MsgLine[34473:7749218] person walk
2018-10-29 15:42:23.653429+0800 MsgLine[34473:7749218] resolveInstanceMethod
2018-10-29 15:42:23.653444+0800 MsgLine[34473:7749218] forwardingTargetForSelector
2018-10-29 15:42:23.653464+0800 MsgLine[34473:7749218] Plane fly
2018-10-29 15:42:23.653478+0800 MsgLine[34473:7749218] resolveInstanceMethod
2018-10-29 15:42:23.653490+0800 MsgLine[34473:7749218] forwardingTargetForSelector
2018-10-29 15:42:23.653508+0800 MsgLine[34473:7749218] methodSignatureForSelector
2018-10-29 15:42:23.653530+0800 MsgLine[34473:7749218] resolveInstanceMethod
2018-10-29 15:42:23.653554+0800 MsgLine[34473:7749218] forwardInvocation
2018-10-29 15:42:23.653571+0800 MsgLine[34473:7749218] Spaceship pluckStars
2018-10-29 15:42:23.653586+0800 MsgLine[34473:7749218] resolveInstanceMethod
2018-10-29 15:42:23.653597+0800 MsgLine[34473:7749218] forwardingTargetForSelector
2018-10-29 15:42:23.653607+0800 MsgLine[34473:7749218] methodSignatureForSelector
2018-10-29 15:42:23.653691+0800 MsgLine[34473:7749218] forwardInvocation
2018-10-29 15:42:23.653710+0800 MsgLine[34473:7749218] Spaceship pluckStars
以上内容转载请注明出处,同时也请大家不吝你的关注和下面的赞赏