SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
typedef struct objc_selector *SEL;
objc_selector的定义如下:
struct objc_selector {
char *name; OBJC2_UNAVAILABLE;// 名称
char *types; OBJC2_UNAVAILABLE;// 类型
};
name和types都是char类型。
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型;
我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:
IMP在objc.h中的定义是:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
这个函数使用当前CPU架构实现的标准的C调用约定。第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。
IMP是“implementation”的缩写,它是由编译器生成的一个函数指针。当你发起一个ObjC消息后,这个函数指针决定了最终执行哪段代码。
objc_msgSend定义如下:
id objc_msgSend(id self, SEL op, ...);
在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。这个函数将消息接收者和方法名作为其基础参数,如以下所示:
objc_msgSend(receiver, selector)
如果消息中还有其它参数,则该方法的形式如下所示:
objc_msgSend(receiver, selector, arg1, arg2, ...)
消息发送步骤:
当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程。
其实编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。
值得一提的是在 i386 平台处理返回类型为浮点数的消息时,需要用到objc_msgSend_fpret函数来进行处理,这是因为返回类型为浮点数的函数对应的 ABI(Application Binary Interface) 与返回整型的函数的 ABI 不兼容。此时objc_msgSend不再适用,于是objc_msgSend_fpret被派上用场,它会对浮点数寄存器做特殊处理。不过在 PPC 或 PPC64 平台是不需要麻烦它的。
PS:有木有发现这些函数的命名规律哦?带“Super”的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别代表“struct”和“return”;“fpret”就是“fp”+“ret”,分别代表“floating-point”和“return”。
Runtime会根据类型自动转换成下列某一个函数:
1、objc_msgSend:普通的消息都会通过该函数发送
2、objc_msgSend_stret:消息中有数据结构作为返回值(不是简单值)时,通过此函数发送和接收返回值
3、objc_msgSendSuper:和objc_msgSend类似,这里把消息发送给父类的实例
4、objc_msgSendSuper_stret:和objc_msgSend_stret类似,这里把消息发送给父类的实例并接收返回值
当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中,self引用了接收者对象,而_cmd引用了方法本身的选择器:
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
在IMP那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。
NSObject类中有个methodForSelector:实例方法,你可以用它来获取某个方法选择器对应的IMP,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。
- (IMP)methodForSelector:(SEL)aSelector;
MyClass* myClass = [[MyClass alloc] init];
void (*setter)(id, SEL, BOOL); int i; setter = (void (*)(id, SEL, BOOL))[myClass methodForSelector:@selector(method1)];
for (i = 0 ; i < 1000 ; i++)
{ setter(myClass, @selector(method1), YES); }
PS:methodForSelector:方法是由 Cocoa 的 Runtime 系统提供的,而不是 Objc 自身的特性。
当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform…的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。
通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:
- (BOOL)respondsToSelector:(SEL)aSelector; - (id)performSelector:(SEL)aSelector; - (id)performSelector:(SEL)aSelector withObject:(id)object; - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
if ([self respondsToSelector:@selector(method)]) { [self performSelector:@selector(method)]; }
当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,通过控制台,我们可以看到以下异常信息:
-[MyClass test]: unrecognized selector sent to instance 0x7f8bf367c8e0
这段异常信息实际上是由NSObject的”doesNotRecognizeSelector”方法抛出的。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。
消息转发机制基本上分为三个步骤:
你可以动态地提供一个方法的实现。例如我们可以用@dynamic关键字在类的实现文件中修饰一个属性:
@dynamic propertyName;
这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:和propertyName方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod:和resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:或resolveClassMethod:来给程序员一次动态添加方法实现的机会。我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作
+ (BOOL)resolveClassMethod:(SEL)sel;//处理类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;//处理实例方法
MyClass.m
#import "MyClass.h"
#import <objc/runtime.h>
#import <objc/message.h>
void dynamicMethodIMP(id self, SEL _cmd,id arg1) {
// implementation ....
NSLog(@"我执行了: %@",arg1);
}
void dynamicMethod3IMP(id self, SEL _cmd) {
// implementation ....
NSLog(@"我执行了dynamicMethod3IMP");
}
@implementation MyClass
@dynamic string;//MyClass.h中@property定义了string变量
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(setString:)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
if (aSEL == @selector(method3)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethod3IMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
main.h
MyClass* myClass = [[MyClass alloc] init];
[myClass performSelector:@selector(method3)];
[myClass performSelector:@selector(setString:) withObject:@"111"];
打印:
我执行了dynamicMethod3IMP
我执行了: 111
PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector: 或 instancesRespondToSelector:方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:返回NO。
MyClass.m
#import "MyClass.h"
#import <objc/runtime.h>
#import <objc/message.h>
void dynamicMethodIMP(id self, SEL _cmd,id arg1) {
NSLog(@"我执行了: %@",arg1);
}
@implementation MyClass
+ (BOOL)resolveClassMethod:(SEL)aSEL
{
if (aSEL == @selector(learnClass:)) {
class_addMethod(objc_getMetaClass(class_getName([self class])), aSEL,(IMP)dynamicMethodIMP , "v@:");
return YES;
}
return [super resolveClassMethod:aSEL];
}
@end
main.h
[MyClass performSelector:@selector(learnClass:) withObject:@"111"];
打印:
我执行了: 111
如果在上一步无法处理消息(实例方法),则Runtime会继续调以下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector;
如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
MyClass.h
#import <Foundation/Foundation.h>
@interface OtherClass : NSObject
- (void)test;
@end
@interface MyClass : NSObject
@end
MyClass.m
#import "MyClass.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation OtherClass
- (void)test{
NSLog(@"%@, %p", self, _cmd);
}
@end
@interface MyClass()
{
OtherClass* _otherClass;
}
@end
@implementation MyClass
- (instancetype)init{
self = [super init];
if (self) {
_otherClass = [[OtherClass alloc]init];
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(test)) {
return _otherClass;
}
return [super forwardingTargetForSelector:aSelector];
}
@end
main.h
MyClass *myClass = [[MyClass alloc] init];
[myClass performSelector:@selector(test)];
打印:
<OtherClass: 0x7ff32b73e850>, 0x101de1fc8
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation;
运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。
forwardInvocation:方法的实现有两个任务:
不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。
还有一个很重要的问题,我们必须重写以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。
MyClass.h
#import <Foundation/Foundation.h>
@interface OtherClass : NSObject
- (void)test;
@end
@interface MyClass : NSObject
@end
MyClass.m
#import "MyClass.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation OtherClass
- (void)test{
NSLog(@"- test %@, %p", self, _cmd);
}
@end
@interface MyClass()
{
OtherClass* _otherClass;
}
@end
@implementation MyClass
- (instancetype)init{
self = [super init];
if (self) {
_otherClass = [[OtherClass alloc]init];
}
return self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([OtherClass instancesRespondToSelector:aSelector]) {
signature = [OtherClass instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([OtherClass instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_otherClass];
}
}
main.h
MyClass *myClass = [[MyClass alloc] init];
[myClass performSelector:@selector(test)];
打印:
- test <OtherClass: 0x7f8b424270f0>, 0x10f766f3f
NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation;
运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。
forwardInvocation:方法的实现有两个任务:
不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。
还有一个很重要的问题,我们必须重写以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。
MyClass.h
#import <Foundation/Foundation.h>
@interface OtherClass : NSObject
- (void)test;
@end
@interface MyClass : NSObject
@end
MyClass.m
#import "MyClass.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation OtherClass
- (void)test{
NSLog(@"- test %@, %p", self, _cmd);
}
@end
@interface MyClass()
{
OtherClass* _otherClass;
}
@end
@implementation MyClass
- (instancetype)init{
self = [super init];
if (self) {
_otherClass = [[OtherClass alloc]init];
}
return self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([OtherClass instancesRespondToSelector:aSelector]) {
signature = [OtherClass instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([OtherClass instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_otherClass];
}
}
main.h
MyClass *myClass = [[MyClass alloc] init];
[myClass performSelector:@selector(test)];
打印:
- test <OtherClass: 0x7f8b424270f0>, 0x10f766f3f
NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。
转发和继承相似,可以用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。
消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。
尽管转发很像继承,但是NSObject类不会将两者混淆。像respondsToSelector: 和 isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个Warrior对象如果被问到是否能响应negotiate消息: