Objective-C Runtime(一):Selector 和方法签名

Selector

在日常开发中,会经常接触到 Selector,例如常用到的 Target-Action 模式中,Action 就是一个 Selector 类型。所以 Selector 大家想必都很熟悉。

Selector 是一个字符串,用于指代某个类方法或实例方法。

例如一个类有下面两个方法:

1
2
- (NSString *)description;
- (NSInteger)addNumber1:(NSInteger)number1 andNumber2:(NSInteger)number2;

他们的选择器分别为:

1
2
"description"
"addNumber1:andNumber2:"

对于有两个或两个以上参数的方法,如果某个参数的名称为空,那么方法的 Selector 的对应部分也为空。例如:

1
- (NSInteger)add:(NSInteger)number1 :(NSInteger)number2;

上面方法的 Selector 为:

1
"add::"

Selector 可以被发送给某个对象或类来进行方法调用,这时被发送的 Selector 需要带上输入参数。例如:

1
[calculator addNumber1:40 andNumber:2];

这里就是把一个 Selector addNumber1:andNumber2 带上了参数发送给了 calulator 对象。

SEL 类型

SEL,即 Selector 类型,是 Objective-C 的一个特殊的类型,它是一个唯一的标识符,来代表某个 Selector。相同的 Selector 的 SEL 标识符一定也是相同的。

创建 SEL 类型的变量有两种方式:

编译时创建:
用 @selector 关键字来 创建,这时代码补全功能能在当前位置寻找已经声明过的 Selector,同时编译器也会检查 Selector 的合法性。

1
SEL selector = @selector(addNumber1:andNumber2:);

运行时创建:
用 NSSelectorFromString 创建,参数用一个字符串类型来代表传入的 Selector,运行时可以根据需要来传入不同的 Selector:

1
SEL selector = NSSelectorFromString(@"addNumber1:andNumber2:");

方法签名

方法签名定义了一个方法的参数和返回值的类型。

方法签名在日常开发中使用的频度不高,但是它必须得存在,它存在的意义是什么呢?

这就要提到 Objective-C 进行消息传递的机制了,Objective-C 中给对象发送一个消息,例如 [object description],会被编译成一个 C 函数的调用(objc_msgSend),这个 C 函数的生成需要知道两个信息,一个是函数名及其参数列表,一个是参数的类型和返回值的类型。

函数名和参数列表可以从 Selector 中获得,参数和返回值类型则是从方法签名中获得。因此为了确定一个方法,Selector 和方法签名缺一不可。

编译器在编译时会检查每个方法的声明,从中解析出 Selector 和方法签名。

签名不匹配

由于 Objective-C 在方法调用(消息传递)时,具有动态类型(Dynamic typing)的特性,消息的接收者的类型在运行时才能确定。对于可能为任何类型的接收者,例如接收者为 id 类型,因为编译器只能基于方法声明上取得方法签名,这时编译器取得的方法签名只是一个推测值,这就有可能会发生签名不匹配。当发生签名不匹配时,这类情况会导致各种运行时错误,如栈溢出、无效的输入输出参数或无效的返回值等。

例如有下面三个类:

1
2
3
4
5
6
7
8
9
10
11
@interface Calculator1 : NSObject
- (int)addNumber1:(NSInteger)number1 andNumber2:(NSInteger)number2;
@end

@interface Calculator2 : NSObject
- (float)addNumber1:(float)number1 andNumber2:(float)number2;
@end

@interface Calculator3 : NSObject
- (NSInteger)addNumber1:(NSInteger)number1 andNumber2:(NSInteger)number2;
@end

对于有可能是这三种类型中的任何一种的接收者,编译器推测的方法类型可能是错误的。如下面的消息传递:

1
[receiver addNumber1:25 andNumber2:10]

编译器在生成 C 函数时,使用的 Selector 为 addNumber1:andNumber2,方法签名为 (NSInteger, NSInteger) -> int

然后实际消息发送中,发现 receiver 是 Calculator2 类型,就会发生方法不匹配,从而发生运行时错误。

怎样避免发生签名不匹配

避免这类问题发生的办法就是对于方法签名相同的方法,尽量采用不同的名字。