1、方法method和selector(选择子)有什么关系
在 Objective-C 中,selector,Method 和 implementation(IMP) 都是 Runtime 的组成部分。在实际开发中它们常常是可以相互转换来处理消息的发送的。选择子代表方法在 Runtime 期间的标识符。为 SEL 类型,虽然 SEL 是 objc_selector 结构体指针,但实际上它只是一个 C 字符串。在类加载的时候,编译器会生成与方法相对应的选择子,并注册到 Objective-C 的 Runtime 运行系统。
得出结论:选择子其实是方法的名称,不同类中方法名相同参数不同的俩个方法,他们的选择子是相同的。
Method的结构体
/// Method
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
};
- 方法名 method_name 类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
- 方法类型 method_types 是个 char 指针,其实存储着方法的参数类型和返回值类型,即是 Type Encoding 编码。(即类型编码)
- method_imp 指向方法的实现,本质上是一个函数的指针,就是前面讲到的 Implementation。
消息传递
在OC中,给对象发送消息的语法是:
id returnValue = [someObject messageName:parameter];
这里,someObject叫做
接收者(receiver)
,messageName:叫做选择子(selector)
,选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,它的原型如下:
void objc_msgSend(id self, SEL cmd, ...)
第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,数量是可变的·,所以这个函数就是参数个数可变的函数。
因此,上述以OC形式展现出来的函数就会转化成如下函数:
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
可以看出,在调用方法时,编译器将它转成了objc_msgSend
消息发送了,在Runtime的执行过程如下
- 1、Runtime先通过对象
someobject
找到isa
指针,判断isa指针是否为nil
,为nil
直接return。 - 2、若不为空则通过
isa
指针找到当前实例的类对象,在类对象下查找缓存是否有messageName
方法。 - 3、若在类对象缓存中找到
messageName
方法,则直接调用IMP
方法(本质上是函数的指针)。 - 4、若在类对象缓存中没找到
messageName
方法,则查找当前类对象的方法列表methodlist
,若找到方法则将其添加到类对象的缓存中。 - 5、若在类对象方法列表中没找到
messageName
方法,则继续到当前类的父类中以相同的方式查找(即类的缓存->类的方法列表)。 - 6、若在父类中找到
messageName
方法,则将IMP
添加到类对象缓存中。 - 7、若在父类中没找到
messageName
方法,则继续查询父类的父类,直到追溯到最上层NSObject
。 - 8、若还是没有找到,则启用动态方法解析、备用接收者、消息转发三部曲,给程序最后一个机会
- 9、若还是没找到,则Runtime会抛出异常
doesNotRecognizeSelector
。
综上,方法的查询流程基本就是查询类对象中的缓存和方法列表->父类中的缓存和方法列表->父类的父类中的缓存和方法列表->...->NSObject中的缓存和方法列表->动态方法解析->备用接收者->消息转发。
消息动态解析
Objective-C 运行时会调用 +resolveInstanceMethod:
或者 +resolveClassMethod:
,让你有机会提供一个函数实现。前者在 对象方法未找到时 调用,后者在 类方法未找到时 调用。我们可以通过重写这两个方法,添加其他函数实现,并返回 YES
, 那运行时系统就会重新启动一次消息发送的过程。
主要用的的方法如下:
// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;
/**
* class_addMethod 向具有给定名称和实现的类中添加新方法
* @param cls 被添加方法的类
* @param name selector 方法名
* @param imp 实现方法的函数指针
* @param types imp 指向函数的返回值与参数类型
* @return 如果添加方法成功返回 YES,否则返回 NO
*/
BOOL class_addMethod(Class cls, SEL name, IMP imp,
const char * _Nullable types);
测试代码
main.m 文件中
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
Person *xiaoming = [[Person alloc]init];
[xiaoming performSelector:@selector(way)];
return 0;
}
person.m 文件中
// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(way)) {
class_addMethod([self class], sel,class_getMethodImplementation([self class], @selector(method)), "123");//使用class_addMethod动态添加方法method
}
return YES;
}
- (void)method{
NSLog(@"进入了消息动态解析");
}
运行了上述的测试代码后,我们会发现即便我们并没有实现way方法,而且使用了performSelector去强行调用way方法,但是我们的程序并没有崩溃,是因为在查找了类方法和所有的父类后还是没有找到way方法,程序进入了消息动态解析,然后我们使用了class_addMethod去动态添加方法method,最后程序从调用
performSelector和直接调用方法的区别
performSelector: withObject:
是在iOS中的一种方法调用方式。他可以向一个对象传递任何消息,而不需要在编译的时候声明这些方法。所以这也是runtime
的一种应用方式。
所以performSelector
和直接调用方法的区别就在与runtime
。直接调用编译是会自动校验。如果方法不存在,那么直接调用 在编译时候就能够发现,编译器会直接报错。
但是使用performSelector
的话一定是在运行时候才能发现,如果此方法不存在就会崩溃。所以一般使用performSelector
的时候,一般都会使用- (BOOL)respondsToSelector:(SEL)aSelector;
来在运行时判断对象是否响应此方法。
消息接受者重定向(备用接受者)
如果上一步中 +resolveInstanceMethod:
或者 +resolveClassMethod:
没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。
如果当前对象实现了 -forwardingTargetForSelector:
或者 +forwardingTargetForSelector:
方法,Runtime 就会调用这个方法,允许我们将消息的接受者转发给其他对象。
其中用到的方法。
// 重定向类方法的消息接收者,返回一个类或实例对象
+ (id)forwardingTargetForSelector:(SEL)aSelector;
// 重定向方法的消息接收者,返回一个类或实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector;
注意:
- 类方法和对象方法消息转发第二步调用的方法不一样,前者是
+forwardingTargetForSelector:
方法,后者是-forwardingTargetForSelector:
方法。- 这里
+resolveInstanceMethod:
或者+resolveClassMethod:
无论是返回YES
,还是返回NO
,只要其中没有添加其他函数实现,运行时都会进行下一步。
测试代码
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(way)) {
Friends *friends = [[Friends alloc]init];
return friends;//返回friends对象,让friends对象接受这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
可以看到,虽然当前 person 没有实现 fun
方法,+resolveInstanceMethod:
也没有添加其他函数实现。但是我们通过 forwardingTargetForSelector
把当前 person
的方法转发给了 friends
对象去执行了。打印结果也证明我们成功实现了转发。
我们通过 forwardingTargetForSelector
可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil
,也不是 self
,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程。
消息重定向
如果经过消息动态解析、消息接受者重定向,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector:
或者 +methodSignatureForSelector:
方法获取函数的参数和返回值类型。
- 如果
methodSignatureForSelector:
返回了一个NSMethodSignature
对象(函数签名),Runtime 系统就会创建一个NSInvocation
对象,并通过forwardInvocation:
消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。 - 如果
methodSignatureForSelector:
返回nil
。则 Runtime 系统会发出doesNotRecognizeSelector:
消息,程序也就崩溃了。
所以我们可以在 forwardInvocation:
方法中对消息进行转发。
注意:类方法和对象方法消息转发第三步调用的方法同样不一样。
类方法调用的是:
+ methodSignatureForSelector:
+ forwardInvocation:
+ doesNotRecognizeSelector:
对象方法调用的是:
- methodSignatureForSelector:
- forwardInvocation:
- doesNotRecognizeSelector:
用到的方法:
// 获取类方法函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation;
// 获取对象方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(way)) {
return [NSMethodSignature methodSignatureForSelector:@selector(way)];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = anInvocation.selector;
Friends *f = [[Friends alloc] init];
if([f respondsToSelector:sel]) { // 判断 Person 对象方法是否可以响应 sel
[anInvocation invokeWithTarget:f]; // 若可以响应,则将消息转发给其他对象处理
} else {
[self doesNotRecognizeSelector:sel]; // 若仍然无法响应,则报错:找不到响应方法
}
}
既然 -forwardingTargetForSelector:
和 -forwardInvocation:
都可以将消息转发给其他对象处理,那么两者的区别在哪?
区别就在于 -forwardingTargetForSelector:
只能将消息转发给一个对象。而 -forwardInvocation:
可以将消息转发给多个对象。
以上就是 Runtime 消息转发的整个流程。
消息发送以及转发机制总结
调用 [receiver selector];
后,进行的流程:
-
编译阶段:
[receiver selector];
方法被编译器转换为:-
objc_msgSend(receiver,selector)
(不带参数) -
objc_msgSend(recevier,selector,org1,org2,…)
(带参数)
-
-
运行时阶段:消息接受者
recevier
寻找对应的selector
- 通过
recevier
的isa 指针
找到recevier
的class(类)
; - 在
Class(类)
的cache(方法缓存)
的散列表中寻找对应的IMP(方法实现)
; - 如果在
cache(方法缓存)
中没有找到对应的IMP(方法实现)
的话,就继续在Class(类)
的method list(方法列表)
中找对应的selector
,如果找到,填充到cache(方法缓存)
中,并返回selector
; - 如果在
class(类)
中没有找到这个selector
,就继续在它的superclass(父类)
中寻找; - 一旦找到对应的
selector
,直接执行recevier
对应selector
方法实现的IMP(方法实现)
。 - 若找不到对应的
selector
,Runtime 系统进入消息转发机制。
- 通过
-
运行时消息转发阶段:
- 动态解析:通过重写
+resolveInstanceMethod:
或者+resolveClassMethod:
方法,利用class_addMethod
方法添加其他函数实现; - 消息接受者重定向:如果上一步添加其他函数实现,可在当前对象中利用
forwardingTargetForSelector:
方法将消息的接受者转发给其他对象; - 消息重定向:如果上一步没有返回值为
nil
,则利用methodSignatureForSelector:
方法获取函数的参数和返回值类型。- 如果
methodSignatureForSelector:
返回了一个NSMethodSignature
对象(函数签名),Runtime 系统就会创建一个NSInvocation
对象,并通过forwardInvocation:
消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。 - 如果
methodSignatureForSelector:
返回nil
。则 Runtime 系统会发出doesNotRecognizeSelector:
消息,程序也就崩溃了。
- 如果
- 动态解析:通过重写