OC调用方法被编译转化为如下的形式:
id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
其实,除了objc_msgSend
之外,消息发送的方法还有:objc_msgSend_stret
、objc_msgSendSuper
、objc_msgSendSuper_stret
等,如果消息发送给超类就用带有super
的方法,如果返回值是结构体而不是普通值就用带有stret
的值。
运行时消息发送的详细步骤:
1.检测selector是不是需要忽略的,比如Mac OS开发,有了垃圾回收机制,就不理会retain、release等这些函数了。
2.检测target是否为nil,因为Objc的特性是允许对任何一个空对象发送方法而不会crash,因为会被忽略掉。
3.如果上面两个都过了,就开始找这个类的IMP,先从cache中找,如果可以找到就直接跳到这个函数里去执行。
4.如果cache列表里找不到,就找一下方法列表methodLists;
5.如果类中methodLists找不到,就到超类的methodList中去找。一直找,直到找到NSObject类中为止。
6.如果还找不到,Runtime提供了三种方法来处理:动态方法解析、消息接受者重定向、消息重定向。
动态方法解析(resolveClassMethod)
所谓动态解析,可以理解为在methodLists中没有找到方法时,动态添加方法的一种策略。主要步骤如下:
// OC方法:
// 类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel
// 实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
// Runtime方法:
/**
运行时方法:向指定类中添加特定方法实现的操作
@param cls 被添加方法的类
@param name selector方法名
@param imp 指向实现方法的函数指针
@param types imp函数实现的返回值和参数类型
@return 添加方法是否成功
*/
BOOL class_addMethod(Class _Nullable cls,
SEL _Nonnull name,
IMP _Nonnull imp,
const char * _Nullable types)
以一个具体的例子来说明,Person中声明了两个方法,一个实例方法、一个类方法,但是没有实现。然后使用runtime为它动态的添加方法的实现。
@interface MFPerson : NSObject
// 声明类方法,但未实现
+ (void)haveMeal:(NSString *)food;
// 声明实例方法,但未实现
- (void)singSong:(NSString *)name;
@end
#import "MFPerson.h"
#import <objc/runtime.h>
@implementation MFPerson
// 重写父类方法:处理类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(haveMeal:)) {
Class class = objc_getMetaClass(object_getClassName(self));
class_addMethod(class,
sel,
class_getMethodImplementation(class, @selector(mf_haveMeal:)),
"v@");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(singSong:)) {
class_addMethod([self class],
sel,
class_getMethodImplementation(self, @selector(mf_singSong:)),
"v@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
+ (void)mf_haveMeal:(NSString *)food {
NSLog(@"%s", __func__);
}
- (void)mf_singSong:(NSString *)name {
NSLog(@"%s", __func__);
}
@end
测试代码:
// 测试:MFPerson类未实现类方法和实例方法,但是未崩溃
[MFPerson haveMeal:@"方便面"];
MFPerson *person = [[MFPerson alloc] init];
[person singSong:@"忽然"];
注意点:
1.class_addMethod中的特殊参数"v@",具体参考:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
2.成功使用动态方法解析还有个前提,就是必须存在可以处理消息的方法,比如上述例子中的:zs_haveMeal、zs_singSong。
消息接收者重定向(forwardingTargetForSelector)
前面方法动态解析的两个resolve方法都是返回BOOL值,当返回的是YES的时候即可正常执行,如果返回NO,消息发送机制就进行到了消息转发(Forwarding)阶段了,可以像下面这样,使用runtime更改消息接收者为其他对象,从而保证程序的正常运行。
// 重定向类方法的消息接受者,返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector
// 重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector
举例说明,先创建一个Student类,声明两个方法,同时实现。
@interface MFStudent : NSObject
// 类方法
+ (void)takeExam:(NSString *)exam;
// 实例方法
- (void)learnKnowledge:(NSString *)course;
@end
然后在TestViewController.m控制器中测试,先添加如下两个方法:
// 重定向类方法的消息接受者,返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(takeExam:)) {
return [MFStudent class];
}
return [super forwardingTargetForSelector:aSelector];
}
// 重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(learnKnowledge:)) {
return self.student;
}
return [super forwardingTargetForSelector:aSelector];
}
在viewDidLoad方法中:
[MessageForwardController performSelector:@selector(takeExam:)
withObject:@"语文"];
self.student = [[MFStudent alloc] init];
[self performSelector:@selector(learnKnowledge:)
withObject:@"天文学知识"];
结果:
+[MFStudent takeExam:]
-[MFStudent learnKnowledge:]
消息重定向(forwardInvocation)
当以上两个方法失效,那么这个时候该对象会因为找不到相应的方法而无法进行响应。此时,runtime会通过forwardInvocation:消息通知该对象,给予此次消息发送最后一次寻找IMP的机会:
- (void)forwardInvocation:(NSInvocation *)anInvocation;
其实每个对象都从NSObject中继承了forwardInvocation:方法,但是NSObject中的这个方法只是简单的调用了doesNotRecongnizeSelector:方法,提示我们错误。所以,我们也可以通过重写这个方法,对不能处理的消息进行一些默认的处理,也可以将消息转发给其他的对象来处理,而不是抛出错误。
我们注意到anInvocation是forwardInvocation的唯一参数,它封装了原始的消息和消息参数。正是因为它,我们还得重写一个方法:methodSignatureForSelector。这是因为在forwardInvocation消息发送之前,runtime系统会向对像发送methodSignatureForSelector消息,并取到返回的方法签名用于生成anInvocation。
下面使用一个示例来重新定义转发逻辑:在上面的TestViewController添加如下代码:
@interface MFStudent : NSObject
// 类方法
+ (void)takeExam:(NSString *)exam;
// 实例方法
- (void)learnKnowledge:(NSString *)course;
@end
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 1.从anInvocation中获取消息
SEL sel = anInvocation.selector;
// 2.判断student方法是否可以响应sel
if ([self.student respondsToSelector:sel]) {
// 2.1.若可以响应,则将消息转发给其他对象处理
[anInvocation invokeWithTarget:self.student];
} else {
// 2.2.若仍无法响应,则报错:找不到响应方法
[self doesNotRecognizeSelector:sel];
}
}
/**
需要从这个方法中获取的信息来创建NSInvocation对象,
因此我们必须重写这个方法,为给定的selector一个合适的方法签名
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
signature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
return signature;
}
@implementation MessageForwardController
- (void)viewDidLoad {
[super viewDidLoad];
self.student = [[MFStudent alloc] init];
[self performSelector:@selector(learnKnowledge:)
withObject:@"英语"];
}
结果:
-[MFStudent learnKnowledge:]
总结:
1.从以上代码可以看出,forwardingTargetForSelector仅支持返回一个对象,而forwardInvocation则不同,可以转发给任意多个对象。