Objective-C选择器(SEL)语法与@selector()原理
1. 选择器(SEL)基础概念
在Objective-C中,选择器(Selector)是一种表示方法名称的数据类型。选择器是运行时系统的核心概念之一,它在消息传递机制中扮演着至关重要的角色。
从本质上讲,选择器是一个指向方法名称的指针。在编译时,编译器会将方法名称转换为选择器。这使得运行时系统能够快速地根据选择器找到对应的方法实现。
在Objective-C中,SEL
是一个类型别名,定义如下:
typedef struct objc_selector *SEL;
这表明 SEL
实际上是一个指向 objc_selector
结构体的指针。虽然开发者通常不需要直接操作 objc_selector
结构体,但了解它是选择器的底层实现有助于深入理解选择器的工作原理。
2. 选择器的唯一性
每个方法名称在程序中都对应一个唯一的 SEL
。这意味着,无论在程序的哪个地方定义了具有相同名称的方法,它们都会对应同一个 SEL
。这种唯一性确保了运行时系统能够准确无误地找到方法的实现。
例如,假设有两个不同的类,ClassA
和 ClassB
,它们都定义了一个名为 doSomething
的方法:
@interface ClassA : NSObject
- (void)doSomething;
@end
@implementation ClassA
- (void)doSomething {
NSLog(@"ClassA doSomething");
}
@end
@interface ClassB : NSObject
- (void)doSomething;
@end
@implementation ClassB
- (void)doSomething {
NSLog(@"ClassB doSomething");
}
@end
尽管 ClassA
和 ClassB
中的 doSomething
方法实现不同,但它们共享同一个 SEL
。这是因为选择器是基于方法名称生成的,而不是基于类或者方法的实现。
3. @selector() 语法
@selector()
是Objective-C中的一个编译器指令,用于获取一个方法对应的选择器。其语法非常简单,只需要在 @selector
后面的括号中写上方法名称即可。
例如,对于前面定义的 ClassA
类,可以通过以下方式获取 doSomething
方法的选择器:
SEL selector = @selector(doSomething);
如果方法带有参数,同样可以在 @selector()
中表示。例如,假设有一个方法 setName:age:
:
@interface Person : NSObject
- (void)setName:(NSString *)name age:(NSInteger)age;
@end
@implementation Person
- (void)setName:(NSString *)name age:(NSInteger)age {
NSLog(@"Name: %@, Age: %ld", name, (long)age);
}
@end
获取该方法选择器的方式如下:
SEL selector = @selector(setName:age:);
注意,在表示带参数的方法选择器时,冒号是方法名称的一部分,不能省略。每个参数对应的冒号都必须准确包含在 @selector()
中。
4. @selector() 与动态方法解析
在运行时,当向一个对象发送消息时,运行时系统首先会根据消息的选择器在对象的类的方法列表中查找对应的方法实现。如果在类的方法列表中没有找到,运行时系统会进入动态方法解析阶段。
在动态方法解析阶段,运行时系统会给类一次机会来动态添加方法实现。这是通过 + (BOOL)resolveInstanceMethod:(SEL)sel
(实例方法)或 + (BOOL)resolveClassMethod:(SEL)sel
(类方法)方法来实现的。
例如,假设有一个类 DynamicClass
,我们希望在运行时动态添加一个方法:
@interface DynamicClass : NSObject
@end
@implementation DynamicClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod(self, sel, (IMP)dynamicMethodImplementation, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void dynamicMethodImplementation(id self, SEL _cmd) {
NSLog(@"Dynamic method called");
}
@end
在上述代码中,当向 DynamicClass
的实例发送 dynamicMethod
消息时,如果该方法在类的方法列表中不存在,运行时系统会调用 + (BOOL)resolveInstanceMethod:(SEL)sel
。在这个方法中,我们检查传入的选择器是否是 @selector(dynamicMethod)
,如果是,则通过 class_addMethod
动态添加方法实现。
这里 @selector()
起到了关键作用,它使得我们能够在运行时准确地判断需要动态添加的方法是哪一个。
5. @selector() 与消息转发
如果动态方法解析阶段没有成功添加方法实现,运行时系统会进入消息转发阶段。消息转发分为快速转发和完整转发两个步骤。
在快速转发阶段,运行时系统会调用 -(id)forwardingTargetForSelector:(SEL)aSelector
方法。这个方法允许对象将消息转发给其他对象来处理。
例如,假设有两个类 ClassX
和 ClassY
,ClassX
希望将某些消息转发给 ClassY
:
@interface ClassY : NSObject
- (void)forwardedMethod;
@end
@implementation ClassY
- (void)forwardedMethod {
NSLog(@"Forwarded method in ClassY");
}
@end
@interface ClassX : NSObject
@property (nonatomic, strong) ClassY *helper;
@end
@implementation ClassX
- (instancetype)init {
self = [super init];
if (self) {
_helper = [[ClassY alloc] init];
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(forwardedMethod)) {
return self.helper;
}
return nil;
}
@end
在上述代码中,当 ClassX
的实例接收到 forwardedMethod
消息且自身没有该方法实现时,运行时系统会调用 forwardingTargetForSelector:
。在这个方法中,我们判断选择器是否是 @selector(forwardedMethod)
,如果是,则返回 ClassY
的实例 self.helper
,从而将消息转发给 ClassY
来处理。
如果快速转发没有找到合适的目标,运行时系统会进入完整转发阶段。在完整转发阶段,运行时系统会调用 -(void)forwardInvocation:(NSInvocation *)anInvocation
方法。在这个方法中,开发者可以更加灵活地处理消息转发,例如修改参数、调用多个方法等。
6. SEL 与 IMP
SEL
(选择器)和 IMP
(Implementation)是紧密相关但又不同的概念。SEL
是方法名称的标识,而 IMP
是方法实现的指针。
IMP
定义如下:
typedef id (*IMP)(id, SEL, ...);
这是一个函数指针类型,它指向方法的具体实现。第一个参数 id self
表示接收消息的对象,第二个参数 SEL _cmd
表示当前调用的选择器,后面的 ...
表示可变参数列表。
例如,对于前面 ClassA
中的 doSomething
方法,其对应的 IMP
指向 -(void)doSomething
方法的实现代码块。
在运行时,当通过 SEL
找到对应的方法实现时,实际上就是找到了对应的 IMP
。然后运行时系统会调用这个 IMP
所指向的函数,从而执行方法的具体逻辑。
7. 获取类的所有选择器
有时候,我们可能需要获取一个类的所有选择器。在Objective-C中,可以通过运行时库函数来实现。
例如,以下代码展示了如何获取 NSString
类的所有实例方法选择器:
unsigned int methodCount;
Method *methods = class_copyMethodList([NSString class], &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
NSString *selectorString = NSStringFromSelector(selector);
NSLog(@"Selector: %@", selectorString);
}
free(methods);
在上述代码中,class_copyMethodList
函数用于获取类的方法列表,method_getName
函数用于从方法结构体中获取选择器,NSStringFromSelector
函数将选择器转换为字符串以便于打印。最后,通过 free
释放 class_copyMethodList
分配的内存。
8. 选择器的内存管理
由于选择器是基于方法名称生成且具有唯一性,它们在程序运行过程中是共享的,并且不需要开发者手动进行内存管理。
一旦程序启动,运行时系统会为每个唯一的方法名称创建一个对应的 SEL
,并且在程序的整个生命周期内,这个 SEL
会一直存在,不会被释放。这是因为选择器在消息传递机制中起着核心作用,频繁地创建和销毁选择器会严重影响性能。
例如,无论多少次通过 @selector()
获取同一个方法的选择器,得到的都是同一个 SEL
实例,不会有新的内存分配。
9. 使用选择器进行动态调用
除了通过常规的消息发送方式调用方法,还可以利用选择器进行动态调用。这在一些需要根据运行时条件决定调用哪个方法的场景中非常有用。
例如,假设有一个类 Calculator
,包含加、减、乘、除四个方法:
@interface Calculator : NSObject
- (NSInteger)add:(NSInteger)a b:(NSInteger)b;
- (NSInteger)subtract:(NSInteger)a b:(NSInteger)b;
- (NSInteger)multiply:(NSInteger)a b:(NSInteger)b;
- (NSInteger)divide:(NSInteger)a b:(NSInteger)b;
@end
@implementation Calculator
- (NSInteger)add:(NSInteger)a b:(NSInteger)b {
return a + b;
}
- (NSInteger)subtract:(NSInteger)a b:(NSInteger)b {
return a - b;
}
- (NSInteger)multiply:(NSInteger)a b:(NSInteger)b {
return a * b;
}
- (NSInteger)divide:(NSInteger)a b:(NSInteger)b {
return a / b;
}
@end
现在,我们希望根据用户输入的操作符动态调用相应的方法:
Calculator *calculator = [[Calculator alloc] init];
NSString *operation = @"add";
SEL selector;
if ([operation isEqualToString:@"add"]) {
selector = @selector(add:b:);
} else if ([operation isEqualToString:@"subtract"]) {
selector = @selector(subtract:b:);
} else if ([operation isEqualToString:@"multiply"]) {
selector = @selector(multiply:b:);
} else if ([operation isEqualToString:@"divide"]) {
selector = @selector(divide:b:);
}
if ([calculator respondsToSelector:selector]) {
NSInteger result = [calculator performSelector:selector withObject:@(5) withObject:@(3)];
NSLog(@"Result: %ld", (long)result);
}
在上述代码中,我们首先根据用户输入的操作符字符串获取对应的选择器。然后通过 respondsToSelector:
方法检查对象是否响应该选择器。如果响应,则使用 performSelector:withObject:withObject:
方法动态调用方法并传递参数。
10. 选择器在通知中心(NSNotificationCenter)中的应用
选择器在通知中心(NSNotificationCenter
)中也有广泛应用。当注册一个观察者来监听某个通知时,需要指定一个选择器,该选择器对应的方法会在通知发布时被调用。
例如,假设有一个类 ObserverClass
,希望监听系统的低电量通知:
@interface ObserverClass : NSObject
- (void)handleLowPowerNotification:(NSNotification *)notification;
@end
@implementation ObserverClass
- (void)handleLowPowerNotification:(NSNotification *)notification {
NSLog(@"Low power notification received");
}
@end
// 注册观察者
ObserverClass *observer = [[ObserverClass alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:observer selector:@selector(handleLowPowerNotification:) name:NSProcessInfoPowerStateDidChangeNotification object:nil];
在上述代码中,通过 addObserver:selector:name:object:
方法注册观察者时,我们指定了 @selector(handleLowPowerNotification:)
作为通知到达时要调用的方法的选择器。这样,当系统发布 NSProcessInfoPowerStateDidChangeNotification
通知时,ObserverClass
的 handleLowPowerNotification:
方法就会被调用。
11. 选择器与协议(Protocol)
协议是一种定义方法列表但不提供实现的机制。在协议中定义的方法,遵守该协议的类需要实现这些方法。选择器在协议的实现和调用过程中同样起着重要作用。
当一个类遵守某个协议时,编译器会检查该类是否实现了协议中定义的所有必需方法。这里的方法检查实际上就是基于选择器进行的。
例如,定义一个协议 MyProtocol
:
@protocol MyProtocol <NSObject>
@required
- (void)requiredMethod;
@optional
- (void)optionalMethod;
@end
假设有一个类 MyClass
遵守该协议:
@interface MyClass : NSObject <MyProtocol>
@end
@implementation MyClass
- (void)requiredMethod {
NSLog(@"Required method implementation");
}
@end
在编译时,编译器会检查 MyClass
是否实现了 @selector(requiredMethod)
方法。如果没有实现,编译器会发出警告(对于 @required
方法)。
在运行时,当通过协议类型的变量调用方法时,同样是基于选择器来查找方法实现。例如:
id<MyProtocol> protocolObject = [[MyClass alloc] init];
if ([protocolObject respondsToSelector:@selector(requiredMethod)]) {
[protocolObject performSelector:@selector(requiredMethod)];
}
在上述代码中,我们首先创建了一个遵守 MyProtocol
协议的对象,并将其赋值给 id<MyProtocol>
类型的变量。然后通过 respondsToSelector:
检查对象是否响应 @selector(requiredMethod)
方法,如果响应则通过 performSelector:
调用该方法。
12. 选择器的局限性与注意事项
虽然选择器在Objective-C中是一个强大的工具,但也存在一些局限性和需要注意的地方。
首先,由于选择器是基于方法名称的,在不同的类中,相同名称的方法会共享同一个选择器。这可能会导致在某些复杂场景下出现混淆,尤其是在多个类继承关系复杂且存在同名方法的情况下。
其次,使用 @selector()
获取选择器时,编译器不会检查方法是否存在。这意味着如果拼写错误方法名称,在编译时不会报错,只有在运行时发送消息找不到方法实现时才会引发异常。
例如:
@interface SomeClass : NSObject
- (void)correctMethod;
@end
@implementation SomeClass
- (void)correctMethod {
NSLog(@"Correct method");
}
@end
// 错误的选择器获取
SEL wrongSelector = @selector(corectMethod); // 拼写错误
SomeClass *obj = [[SomeClass alloc] init];
if ([obj respondsToSelector:wrongSelector]) {
[obj performSelector:wrongSelector];
} else {
NSLog(@"Selector not recognized");
}
在上述代码中,由于 @selector(corectMethod)
拼写错误,虽然编译时不会报错,但运行时 respondsToSelector:
会返回 NO
,因为实际上并不存在这个方法的选择器。
另外,在使用选择器进行动态调用时,由于编译器无法对参数类型进行检查,可能会导致运行时错误。例如,在动态调用方法时传递了错误类型的参数,可能会引发未定义行为。
13. 总结选择器在Objective-C体系中的地位
选择器(SEL)是Objective-C语言运行时系统的基石之一,贯穿于消息传递、动态方法解析、消息转发等重要机制中。它通过将方法名称映射为唯一的标识,使得运行时系统能够高效地查找和调用方法实现。
@selector()
编译器指令为开发者提供了一种方便获取选择器的方式,无论是在常规的消息发送,还是在动态调用、通知中心、协议等各种场景中,选择器都发挥着不可或缺的作用。
尽管选择器有其局限性和需要注意的地方,但合理利用选择器能够极大地提升代码的灵活性和可扩展性,充分发挥Objective-C动态特性的优势。深入理解选择器的语法和原理,对于编写高质量、健壮的Objective-C程序至关重要。无论是开发iOS应用、macOS应用还是其他基于Objective-C的项目,掌握选择器相关知识都是开发者必备的技能。