Runtime系列5之-消息转发
这一章,我们就要开始讨论Runtime中最有意思的一部分:消息处理机制。我们将详细讨论消息的发送及消息的转发。其实这个也应该是方法的一部分,因为方法就是消息.
方法调用
先了解一下方法调用相关的知识
方法调用流程
-
在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式
[receiver message]
转化为一个消息函数的调用,即objc_msgSend
。这个函数将消息接收者和方法名作为其基础参数,如以下所示:objc_msgSend(receiver, selector)
-
如果消息中还有其它参数,则该方法的形式如下所示:
objc_msgSend(receiver, selector, arg1, arg2, ...)
- 这个函数完成了动态绑定的所有事情:
- 首先它找到
selector
对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。 - 它调用方法实现,并将接收者对象及方法的所有参数传给它。
- 最后,它将实现返回的值作为它自己的返回值。
- 首先它找到
- 消息的关键在于我们前面章节讨论过的结构体objc_class,这个结构体有两个字段是我们在分发消息的关注的:
- 指向父类的指针
- 一个类的方法分发表,即methodLists。
- 当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。
-
下图演示了这样一个消息的基本框架:
- 当消息发送给一个对象时,
objc_msgSend
通过对象的isa
指针获取到类的结构体,然后在方法分发表里面查找方法的selector
。如果没有找到selector
,则通过objc_msgSend
结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector
。依此,会一直沿着类的继承体系到达NSObject
类。一旦定位到selector
,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector
,则会走消息转发流程,这个我们在后面讨论。
- 当消息发送给一个对象时,
隐藏参数
- objc_msgSend有两个隐藏参数:
- 消息接收对象
- 方法的selector
- 这两个参数为方法的实现提供了调用者的信息。之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明。它们是在编译期被插入实现代码的。
获取方法地址
- Runtime中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得那么直接。当然,方法的缓存一定程度上解决了这一问题。
- 我们上面提到过,如果想要避开这种动态绑定方式,我们可以获取方法实现的地址,然后像调用函数一样来直接调用它。特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种方式可以提高程序的性能。
- NSObject类提供了
methodForSelector:
方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:
返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。 -
我们通过以下代码来看看
methodForSelector:
的使用://定义一个函数指针 void (*setter)(id, SEL, BOOL); int i; //拿到一个OC方法的函数指针 setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; //直接调用C函数 for (i = 0 ; i < 1000 ; i++) setter(targetList[i], @selector(setFilled:), YES);
- 这里需要注意的就是函数指针的前两个参数必须是id和SEL。
- 当然这种方式只适合于在类似于for循环这种情况下频繁调用同一方法,以提高性能的情况。另外,methodForSelector:是由Cocoa运行时提供的;它不是Objective-C语言的特性。
消息转发
- 当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?
- 默认情况下,如果是以
[object message]
的方式调用方法,如果object
没有message
消息声明时,编译器会报错。但如果有了声明,就同perform...
的形式来调用一样,则需要等到运行时才能确定object
是否能接收message
消息。如果不能,则程序崩溃。抛出unrecognized selector sent to …
类似这样的异常信息. - 但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。(后两种为消息转发)
- Method Resolution
- Fast Forwarding
- Normal Forwarding
-
通常,当我们不能确定一个对象是否能接收某个消息时,会先调用
respondsToSelector:
来判断一下。如下代码所示:if ([self respondsToSelector:@selector(method)]) { [self performSelector:@selector(method)]; }
-
不过,我们这边想讨论下不使用respondsToSelector:判断的情况。这才是我们这一节的重点。
Method Resolution
- 首先Objective-C在运行时调用
+ resolveInstanceMethod:
或+ resolveClassMethod:
方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。 -
举一个简单例子,定义一个类
Message
,它主要定义一个方法sendMessage
,下面就是它的设计与实现:@interface Message : NSObject - (void)sendMessage:(NSString *)word; @end @implementation Message - (void)sendMessage:(NSString *)word { NSLog(@"normal way : send message = %@", word); } @end
-
如果我在viewDidLoad方法中创建Message对象并调用sendMessage方法:
- (void)viewDidLoad { [super viewDidLoad]; Message *message = [Message new]; [message sendMessage:@"Sam Lau"]; }
打印以下信息:
normal way : send message = Sam Lau
-
但现在我将原来sendMessage方法实现给注释掉,覆盖resolveInstanceMethod方法:
#pragma mark - Method Resolution /// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(sendMessage:)) { class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) { NSLog(@"method resolution way : send message = %@", word); }), "v@*"); } return YES; }
打印以下信息:
method resolution way : send message = Sam Lau
-
如果
resolveInstanceMethod
方法返回NO,运行时就跳转到下一步:消息转发(Message Forwarding),下面两种皆为消息转发
-
Fast Forwarding
- 如果目标对象实现
- forwardingTargetForSelector:
方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding
。 -
继续上面
Message
类的例子,将sendMessage
和resolveInstanceMethod
方法注释掉,然后添加forwardingTargetForSelector
方法的实现:#pragma mark - Fast Forwarding - (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(sendMessage:)) { return [MessageForwarding new]; } return nil; }
-
此时还缺一个转发消息的类MessageForwarding,这个类的设计与实现如下:
@interface MessageForwarding : NSObject - (void)sendMessage:(NSString *)word; @end @implementation MessageForwarding - (void)sendMessage:(NSString *)word { NSLog(@"fast forwarding way : send message = %@", word); } @end
打印以下信息:
fast forwarding way : send message = Sam Lau
- 这里叫Fast,是因为这一步不会创建NSInvocation对象,但Normal Forwarding会创建它,所以相对于更快点。
Normal Forwarding
- 如果没有使用Fast Forwarding来消息转发,最后只有使用
Normal Forwarding
来进行消息转发。它首先调用methodSignatureForSelector:
方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance
异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:
方法。 -
继续前面的例子,将
forwardingTargetForSelector
方法注释掉,添加methodSignatureForSelector
和forwardInvocation
方法的实现:#pragma mark - Normal Forwarding - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector]; if (!methodSignature) { methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"]; } return methodSignature; } - (void)forwardInvocation:(NSInvocation *)anInvocation { MessageForwarding *messageForwarding = [MessageForwarding new]; if ([messageForwarding respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:messageForwarding]; } }
三种方法的选择
- Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?
- Method Resolution:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
- Fast Forwarding:它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。
- Normal Forwarding:它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。
消息转发与多重继承
- 回过头来看第二(Fast Forwarding)和第三步(Normal Forwarding),通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。
-
不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如
respondsToSelector:
和isKindOfClass:
只能用于继承体系,而不能用于转发链。便如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:- (BOOL)respondsToSelector:(SEL)aSelector { if ( [super respondsToSelector:aSelector]) return YES; else { /* Here, test whether the aSelector message can * * be forwarded to another object and whether that * * object can respond to it. Return YES if it can. */ } return NO; }
举例: 动态添加方法
- 如果一个类方法非常多,其中可能许多方法暂时用不到。而加载类方法到内存的时候需要给每个方法生成映射表,又比较耗费资源。此时可以使用RunTime动态添加方法;
- 动态给某个类添加方法,相当于懒加载机制,类中许多方法暂时用不到,那么就先不加载,等用到的时候再去加载方法。
- 动态添加方法的方法:
- 首先我们先不实现对象方法,当调用performSelector: 方法的时候,再去动态加载方法。
- 创建
Person
类,使用performSelector:
调用Person
类对象的eat
方法。 - 此时编译的时候是不会报错的,程序运行时才会报错,因为
Person
类中并没有实现eat
方法,当去类中的Method List
中发现找不到eat
方法,会报错找不到eat方法。 - 而当找不到对应的方法时就会来到拦截调用,在找不到调用的方法程序崩溃之前调用的方法。
- 当调用了没有实现的对象方法的时,就会调用
+(BOOL)resolveInstanceMethod:(SEL)sel
方法。 - 当调用了没有实现的类方法的时候,就会调用
+(BOOL)resolveClassMethod:(SEL)sel
方法。 - 我们可以实现方法
resolveInstanceMethod:
或者resolveClassMethod:
方法,动态的给实例方法或者类方法添加方法和方法实现。 - 所以通过这两个方法就可以知道哪些方法没有实现,从而动态添加方法。参数sel即表示没有实现的方法。
- 一个objective - C方法最终都是一个C函数,默认任何一个方法都有两个参数。
self
: 方法调用者_cmd
: 调用方法编号。我们可以使用函数class_addMethod
为类添加一个方法以及实现。
class_addMethod函数
-
举例之前我们先详细的讲一下这个函数
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
class_addMethod
中的四个参数。第一,二个参数比较好理解,重点是第三,四个参数。- cls : 表示给哪个类添加方法,这里要给Person类添加方法,self即代表Person。
- SEL name : 表示添加方法的编号。因为这里只有一个方法需要动态添加,并且之前通过判断确定sel就是eat方法,所以这里可以使用sel。
- IMP imp : 表示方法的实现,函数入口,函数名可与方法名不同(建议与方法名(SEL中的name)相同)需要自己来实现这个函数。每一个方法都默认带有两个隐式参数
self
: 方法调用者_cmd
: 调用方法的标号,可以写也可以不写。 - types : 表示方法类型,需要用特定符号。系统提供的例子中使用的是
"v@:"
,我们来到API中看看"v@:"
指定的方法是什么类型的。- 从苹果官方的Type Encoding可以看出:
v -> void
表示无返回值@ -> object
表示id参数: -> method selector
表示SEL
- 从苹果官方的Type Encoding可以看出:
动态创建无参数的方法
//Person.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject
@end
//Person.m文件
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
/*
override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 动态添加eat方法
// 首先判断sel是不是eat方法 也可以转化成字符串进行比较。
if (sel == @selector(eat)) {
class_addMethod(self, sel, (IMP)eat , "v@:");
// 处理完返回YES
return YES;
}
return [super resolveInstanceMethod:sel];
}
/*
这个是C函数,函数名就是函数地址,即指针
IMP也是函数指针因此,这个函数的IMP就是(IMP)eat
*/
void eat(id self ,SEL _cmd)
{
// 实现内容
NSLog(@"%@的%@方法动态实现了",
self,NSStringFromSelector(_cmd));
}
@end
//调用:
Person *p = [[Person alloc]init];
// 当调用 P中没有实现的方法时,动态加载方法
[p performSelector:@selector(eat)];
动态创建有参数的方法
- 如果是有参数的方法,需要对方法的实现和class_addMethod方法内方法类型参数做一些修改。
- 方法实现:因为在C语言函数中,所以对象参数类型只能用id代替。
- 方法类型参数:因为添加了一个id参数,所以方法类型应该为”v@:@”
// Person.m文件
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
+(BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat:)) {
class_addMethod(self, sel, (IMP)aaaa , "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void aaaa(id self ,SEL _cmd,id Num)
{
// 实现内容
NSLog(@"%@的%@方法动态实现了,参数为%@",self,NSStringFromSelector(_cmd),Num);
}
@end
//调用函数
//为参数@"xx_cc"
Person *p = [[Person alloc]init];
[p performSelector:@selector(eat:)withObject:@"xx_cc"];
注意:我们看到person.h的文件中我们并没有方法-(void)eat;
的声明,所以,如果我们用[p eat];
,在编译期就会报错,但是如果我们用[p performSelector:@selector(eat)];
只会有警告,在点击调用时,才会去查方法的实现.