MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

解析Objective-C中SEL类型与@selector的使用

2023-03-122.3k 阅读

一、SEL类型简介

在Objective - C中,SEL(Selector的缩写)是一种数据类型,它代表一个方法的选择器。简单来说,SEL就是一个指向方法的指针,更准确地说,它是一个唯一标识某个类中特定方法的标识符。当编译器遇到一个方法调用时,它会将方法名转换为一个SEL类型的对象。

SEL类型定义在<objc/objc.h>头文件中,其定义如下:

typedef struct objc_selector *SEL;

从定义可以看出,SEL本质上是一个指向objc_selector结构体的指针。虽然我们通常不需要关心objc_selector结构体的具体内容,但了解SEL是指针类型有助于理解它在内存中的行为和使用方式。

二、SEL的唯一性与存储

每个方法的SEL在程序运行期间是唯一的。无论一个类被实例化多少次,或者在不同的类中有相同名字的方法,对应的SEL都是唯一的。这是因为SEL是基于方法名生成的,并且在程序启动时,运行时系统会为每个唯一的方法名生成一个对应的SEL对象,并将其存储在一个全局的表中。

这种唯一性保证了在Objective - C运行时系统中,能够高效地通过SEL找到对应的方法实现。例如,假设有两个不同的类ClassAClassB,它们都有一个名为printMessage的方法:

@interface ClassA : NSObject
- (void)printMessage;
@end

@implementation ClassA
- (void)printMessage {
    NSLog(@"This is ClassA's printMessage");
}
@end

@interface ClassB : NSObject
- (void)printMessage;
@end

@implementation ClassB
- (void)printMessage {
    NSLog(@"This is ClassB's printMessage");
}
@end

尽管ClassAClassB中的printMessage方法属于不同的类,但它们对应的SEL在整个程序中是唯一的。运行时系统可以根据这个唯一的SEL,在不同类的方法列表中找到正确的方法实现。

三、@selector的使用

@selector是Objective - C中的一个编译器指令,它用于将一个方法名转换为对应的SEL对象。其语法很简单,只需要在方法名前加上@selector关键字,例如:@selector(methodName),其中methodName是你要转换的方法名。

1. 无参数方法的@selector使用

对于无参数的方法,使用@selector非常直观。比如,我们有一个简单的类MyClass,其中定义了一个无参数的方法sayHello

@interface MyClass : NSObject
- (void)sayHello;
@end

@implementation MyClass
- (void)sayHello {
    NSLog(@"Hello!");
}
@end

在其他地方,我们可以通过@selector获取sayHello方法的SEL,并使用它来调用方法(稍后会介绍如何通过SEL调用方法):

MyClass *obj = [[MyClass alloc] init];
SEL selector = @selector(sayHello);

这里,selector变量就是sayHello方法对应的SEL对象。

2. 带参数方法的@selector使用

对于带参数的方法,@selector的使用需要注意方法签名的正确表示。方法签名由方法名和参数类型组成。例如,假设有一个类Calculator,其中有一个方法add:and:用于计算两个整数的和:

@interface Calculator : NSObject
- (int)add:(int)a and:(int)b;
@end

@implementation Calculator
- (int)add:(int)a and:(int)b {
    return a + b;
}
@end

要获取这个方法的SEL,需要按照方法签名的格式来写@selector

Calculator *calc = [[Calculator alloc] init];
SEL addSelector = @selector(add:and:);

这里的add:and:就是add:(int)a and:(int)b方法的正确SEL表示。注意,参数名在@selector中并不重要,重要的是方法名和冒号的位置,冒号表示参数的位置。

3. 类方法的@selector使用

类方法同样可以使用@selector获取其SEL。例如,有一个类SingletonClass,它有一个类方法sharedInstance用于获取单例实例:

@interface SingletonClass : NSObject
+ (instancetype)sharedInstance;
@end

@implementation SingletonClass
static SingletonClass *sharedInstance = nil;
+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}
@end

获取类方法sharedInstanceSEL如下:

SEL classMethodSelector = @selector(sharedInstance);

这里获取类方法的SEL和实例方法类似,只是类方法属于类本身,而不是类的实例。

四、通过SEL调用方法

获取了SEL之后,我们可以通过Objective - C的运行时系统来调用对应的方法。这通常使用NSObject类的performSelector:系列方法来实现。

1. performSelector:方法

performSelector:方法用于调用一个无参数的实例方法。例如,我们继续使用前面定义的MyClass类和sayHello方法:

MyClass *obj = [[MyClass alloc] init];
SEL selector = @selector(sayHello);
[obj performSelector:selector];

上述代码中,通过performSelector:selector就调用了MyClass实例objsayHello方法,控制台会输出Hello!

2. performSelector:withObject:方法

如果方法需要一个参数,就可以使用performSelector:withObject:方法。假设我们有一个类Printer,其中有一个方法printString:用于打印字符串:

@interface Printer : NSObject
- (void)printString:(NSString *)string;
@end

@implementation Printer
- (void)printString:(NSString *)string {
    NSLog(@"%@", string);
}
@end

调用这个方法的代码如下:

Printer *printer = [[Printer alloc] init];
SEL printSelector = @selector(printString:);
NSString *message = @"This is a test string";
[printer performSelector:printSelector withObject:message];

这里通过performSelector:printSelector withObject:messagemessage作为参数传递给printString:方法,控制台会输出This is a test string

3. performSelector:withObject:withObject:方法

对于需要两个参数的方法,performSelector:withObject:withObject:方法就派上用场了。比如我们回到前面的Calculator类的add:and:方法:

Calculator *calc = [[Calculator alloc] init];
SEL addSelector = @selector(add:and:);
int result = [calc performSelector:addSelector withObject:@(5) withObject:@(3)];
NSLog(@"The result of 5 + 3 is %d", result);

这里通过performSelector:addSelector withObject:@(5) withObject:@(3)将两个整数53作为参数传递给add:and:方法,并得到计算结果。注意,由于performSelector:withObject:withObject:方法的参数类型是id,所以我们使用NSNumber将基本数据类型int进行了包装。

五、SEL与动态方法解析

在Objective - C中,动态方法解析是一个强大的特性,而SEL在其中起着关键作用。当向一个对象发送一个它在编译时无法识别的消息(即没有找到对应的方法实现)时,运行时系统会启动动态方法解析流程。

首先,运行时系统会调用类的+resolveInstanceMethod:(对于实例方法)或+resolveClassMethod:(对于类方法)类方法,传递未识别的SEL。如果在这个方法中,我们可以为该SEL动态添加一个方法实现,那么消息发送就可以继续进行。

例如,假设我们有一个类DynamicClass,在编译时它没有定义unknownMethod方法:

@interface DynamicClass : NSObject
@end

@implementation DynamicClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(unknownMethod)) {
        class_addMethod(self, sel, (IMP)dynamicMethodImplementation, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void dynamicMethodImplementation(id self, SEL _cmd) {
    NSLog(@"This is a dynamically added method");
}
@end

在上述代码中,+resolveInstanceMethod:方法检查传入的SEL是否是@selector(unknownMethod)。如果是,就使用class_addMethod函数动态添加一个方法实现。class_addMethod的参数分别是类对象、SEL、方法实现的函数指针(IMP)以及方法的类型编码。这里的"v@:"是方法的类型编码,表示该方法返回void,第一个参数是id类型的self,第二个参数是SEL类型的_cmd_cmd代表当前调用的SEL)。

然后我们可以尝试调用这个动态方法:

DynamicClass *dynamicObj = [[DynamicClass alloc] init];
[dynamicObj performSelector:@selector(unknownMethod)];

上述代码会成功调用动态添加的unknownMethod方法,并在控制台输出This is a dynamically added method

六、SEL在消息转发中的作用

当动态方法解析没有成功处理未识别的消息时,Objective - C运行时系统会进入消息转发流程。消息转发分为快速转发和完整转发两个阶段,而SEL在这两个阶段中都起着重要作用。

1. 快速转发(备用接收者)

在快速转发阶段,运行时系统会调用对象的-forwardingTargetForSelector:方法,传递未识别的SEL。如果这个方法返回一个非nil的对象,运行时系统会将消息转发给这个对象处理。

例如,假设有两个类ClassAClassBClassA收到一个它无法处理的消息,但ClassB可以处理这个消息:

@interface ClassA : NSObject
@end

@implementation ClassA
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(handleMessage)) {
        return [[ClassB alloc] init];
    }
    return nil;
}
@end

@interface ClassB : NSObject
- (void)handleMessage;
@end

@implementation ClassB
- (void)handleMessage {
    NSLog(@"ClassB handles the message");
}
@end

在上述代码中,ClassA-forwardingTargetForSelector:方法检查传入的SEL是否是@selector(handleMessage)。如果是,就返回一个ClassB的实例。这样,当向ClassA的实例发送handleMessage消息时,消息会被转发给ClassB的实例处理。

ClassA *a = [[ClassA alloc] init];
[a performSelector:@selector(handleMessage)];

上述代码会在控制台输出ClassB handles the message

2. 完整转发

如果快速转发没有找到合适的备用接收者,运行时系统会进入完整转发阶段。首先,运行时系统会调用-methodSignatureForSelector:方法,传递未识别的SEL,要求返回一个方法签名(NSMethodSignature对象)。如果返回了有效的方法签名,运行时系统会接着调用-forwardInvocation:方法,传递一个NSInvocation对象,这个对象包含了原始的消息发送信息,包括SEL、参数等。

例如,我们对前面的ClassA进行修改,使其进入完整转发流程:

@interface ClassA : NSObject
@end

@implementation ClassA
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(handleMessage)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    ClassB *b = [[ClassB alloc] init];
    if ([b respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:b];
    }
}
@end

@interface ClassB : NSObject
- (void)handleMessage;
@end

@implementation ClassB
- (void)handleMessage {
    NSLog(@"ClassB handles the message in full forwarding");
}
@end

在上述代码中,-methodSignatureForSelector:方法为@selector(handleMessage)返回了一个有效的方法签名。然后在-forwardInvocation:方法中,获取NSInvocation对象中的SEL,创建一个ClassB的实例,并检查ClassB是否响应这个SEL。如果响应,就通过[anInvocation invokeWithTarget:b]将消息转发给ClassB的实例处理。

ClassA *a = [[ClassA alloc] init];
[a performSelector:@selector(handleMessage)];

上述代码会在控制台输出ClassB handles the message in full forwarding

七、SEL的内存管理

由于SEL是由运行时系统管理的全局唯一对象,所以我们不需要手动进行内存管理。SEL对象在程序启动时创建,并在程序运行期间一直存在,直到程序结束。

例如,我们前面获取的各种SEL对象,如@selector(sayHello)@selector(add:and:)等,不需要像普通对象那样调用releaseautorelease或使用ARC来管理其内存。这是因为运行时系统会自动维护SEL对象的生命周期,确保其唯一性和稳定性。

不过,在使用SEL与其他对象交互时,比如在performSelector:系列方法中传递对象参数,需要注意参数对象的内存管理。例如,在[printer performSelector:printSelector withObject:message]中,message对象的内存管理遵循正常的Objective - C内存管理规则(ARC或手动引用计数)。

八、SEL在实际开发中的应用场景

1. 实现回调机制

在很多情况下,我们需要实现回调机制,让一个对象在某个事件发生时通知另一个对象。通过SELperformSelector:方法可以很方便地实现这一点。

例如,假设有一个Downloader类用于下载文件,当下载完成时,它需要通知ViewController类。我们可以在Downloader类中定义一个delegate属性,并在下载完成时通过SEL调用delegate的方法:

@protocol DownloaderDelegate <NSObject>
@optional
- (void)downloadDidFinish;
@end

@interface Downloader : NSObject
@property (nonatomic, weak) id<DownloaderDelegate> delegate;
- (void)startDownload;
@end

@implementation Downloader
- (void)startDownload {
    // 模拟下载过程
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if ([self.delegate respondsToSelector:@selector(downloadDidFinish)]) {
            [self.delegate performSelector:@selector(downloadDidFinish)];
        }
    });
}
@end

@interface ViewController : UIViewController <DownloaderDelegate>
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Downloader *downloader = [[Downloader alloc] init];
    downloader.delegate = self;
    [downloader startDownload];
}

- (void)downloadDidFinish {
    NSLog(@"Download finished");
}
@end

在上述代码中,Downloader类在下载完成后,检查delegate是否响应downloadDidFinish方法(通过SEL判断),如果响应则调用该方法,从而实现了回调机制。

2. 反射机制

SEL与运行时系统结合可以实现类似于反射的功能。通过获取类的方法列表,我们可以根据SEL动态调用不同的方法,而不需要在编译时就确定具体调用哪个方法。

例如,我们有一个类ReflectionClass,其中定义了多个方法,我们可以通过SEL动态调用这些方法:

@interface ReflectionClass : NSObject
- (void)method1;
- (void)method2;
- (void)method3;
@end

@implementation ReflectionClass
- (void)method1 {
    NSLog(@"Method 1 is called");
}

- (void)method2 {
    NSLog(@"Method 2 is called");
}

- (void)method3 {
    NSLog(@"Method 3 is called");
}
@end

// 动态调用方法
ReflectionClass *reflectionObj = [[ReflectionClass alloc] init];
SEL methodSelector = NSSelectorFromString(@"method2");
if (methodSelector) {
    [reflectionObj performSelector:methodSelector];
}

在上述代码中,通过NSSelectorFromString函数将字符串转换为SEL,然后使用performSelector:方法动态调用对应的方法。这种方式在一些需要根据用户输入或配置动态调用方法的场景中非常有用。

3. 简化代码结构

在一些复杂的业务逻辑中,可能存在大量相似的方法调用逻辑。通过使用SELperformSelector:方法,可以将这些相似的逻辑抽象出来,简化代码结构。

例如,假设有一个类ActionExecutor,它需要根据不同的操作类型执行不同的方法:

@interface ActionExecutor : NSObject
- (void)executeAction:(NSString *)actionType;
- (void)action1;
- (void)action2;
- (void)action3;
@end

@implementation ActionExecutor
- (void)executeAction:(NSString *)actionType {
    SEL actionSelector = NSSelectorFromString(actionType);
    if (actionSelector && [self respondsToSelector:actionSelector]) {
        [self performSelector:actionSelector];
    }
}

- (void)action1 {
    NSLog(@"Action 1 is executed");
}

- (void)action2 {
    NSLog(@"Action 2 is executed");
}

- (void)action3 {
    NSLog(@"Action 3 is executed");
}
@end

在其他地方,我们可以这样使用:

ActionExecutor *executor = [[ActionExecutor alloc] init];
[executor executeAction:@"action2"];

通过这种方式,executeAction:方法可以根据传入的字符串动态调用相应的方法,避免了大量的if - elseswitch - case语句,使代码结构更加清晰和易于维护。

九、SEL使用的注意事项

1. SEL的准确性

在使用@selector获取SEL时,一定要确保方法名的准确性。如果方法名拼写错误,编译器不会报错,但运行时可能会导致未定义行为。例如,如果我们将@selector(sayHello)写成@selector(sayHllo),运行时系统将找不到对应的方法实现,可能会引发崩溃。

2. 方法签名的一致性

当通过performSelector:系列方法调用方法时,要确保传递的参数类型和数量与方法签名一致。否则,可能会导致程序崩溃或得到意外的结果。比如,在调用[calc performSelector:addSelector withObject:@(5) withObject:@(3)]时,如果add:and:方法的参数类型不是int,而是其他类型,就会出现问题。

3. 内存管理与对象生命周期

虽然SEL本身不需要手动管理内存,但在使用performSelector:系列方法传递对象参数时,要注意参数对象的内存管理。如果参数对象在方法调用过程中被释放,可能会导致野指针错误。例如,在[printer performSelector:printSelector withObject:message]中,如果message对象在performSelector:调用之前被提前释放,就会出现问题。

4. 动态方法解析与消息转发的性能

动态方法解析和消息转发机制虽然强大,但它们会带来一定的性能开销。在频繁调用的方法中,尽量避免使用这些机制,以免影响程序的性能。如果确实需要使用,要确保在动态方法解析或消息转发过程中执行的代码尽可能高效。

通过深入理解SEL类型与@selector的使用,我们可以更好地利用Objective - C的运行时特性,编写出更加灵活、高效和强大的代码。无论是实现复杂的回调机制、反射功能,还是简化代码结构,SEL都发挥着重要作用。同时,在使用过程中要注意各种细节和潜在问题,以确保程序的稳定性和性能。