前言:我是参考 南峰子 的博客加上自己理解写的,原著专辑大家自己可看:http://southpeak.github.io/categories/objectivec/
一,简介:Method
1. 在OC 中方法我们用Method
来表示,它的定义是一个结构体,如下:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
可以看出其主要是:SEL
、IMP
、char *method_types
三部分组成,我们可以分别来看看:
2. SEL
SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
typedef struct objc_selector *SEL;
方法的selector
用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int
类型的地址),这个标识就是SEL
。如下代码所示:
SEL sel1 = @selector(method1);
NSLog(@"sel : %p", sel1);
上面的输出为:
2014-10-30 18:40:07.518 RuntimeTest[52734:466626] sel : 0x100002d72
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这也就导致Objective-C在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差。
当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP
工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么SEL仅仅是函数名了。
其实说了这么多我总结的就一个SEL
就是根据方法名称得到字符串,其已经忽略了所带的参数是什么,就是在OC中代表某个方法,它最后要和IMP(具体执行的C函数,标准的C调用)进行映射绑定,下面我们就来介绍IMP
3. IMP
实际上是一个函数指针,指向方法实现的首地址。其定义如下:
id (*IMP)(id, SEL, ...)
这个函数使用当前CPU架构实现的标准的C调用约定。第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。
前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL
方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。
总结的说IMP其实就是C函数指针,这个函数必须要传入两个参数第一个是执行该函数的对象,第二个就是关联的SEL,剩下跟着的参数就是方法出入的参数了,你后面跟多少,看你自己的了!
二,方法的调用流程
1,基本调用
在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。这个函数将消息接收者和方法名作为其基础参数,如以下所示
objc_msgSend(receiver, selector)
如果有参数:
objc_msgSend(receiver, selector, arg1, arg2, ...)
1,首先它找到selector对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。
2,它调用方法实现,并将接收者对象及方法的所有参数传给它。
3,最后,它将实现返回的值作为它自己的返回值。
通俗而言,我们在用一个实例或者是类调用一个方法,这个实例或者是类就是Receiver,方法的字符串就是对应selector,这个方法对应的参数就往后面依次排开agr1,arg2.....
2, 消息转发(message forwarding)
这才是真正核心的地方,其实上面讲的基本调用也就是正常转发,但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform...的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:
来判断一下。当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,如下代码所示:
unrecognized selector sent to instance 0x100111940
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940'
这段异常信息实际上是由NSObject的”doesNotRecognizeSelector“方法抛出的。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。
消息转发机制基本上分为三个步骤:
1>,动态方法解析------->2>,备用接收者-------->3>,完整消息转发
1>动态方法解析
对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。
例如我又一个Person类,我在外面通过performSelector
调用了它的一个实例方法testMethod1
,并传入了两个参数
/*******方法传递***********/
Person *testPerson = [[Person alloc]init];
[testPerson performSelector:@selector(testMethod1) withObject:@"小胡" withObject:@"酒鬼"];
在我的Person类中我没有testMethod1
这个方法,我需要怎么处理了?
//C函数中IMP其实就是标准的 C函数调用
void IMPTestMethod1Funcation(id self,SEL _cmd,NSString *name,NSString *nick){
NSLog(@"IMP C 函数中实现:%@, %p", self, _cmd);
NSLog(@"我带入的参数是name:%@ nick:%@ ",name,nick);
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSString *selString = NSStringFromSelector(sel);
NSLog(@"调用的方法是什么:%@",selString);
if ([selString isEqualToString:@"testMethod1"]) {
class_addMethod(self.class, sel, (IMP)IMPTestMethod1Funcation, "@:@@");
}
return [super resolveInstanceMethod:sel];
}
首先,当调用方法testMethod1
在Person找不对应的方法,会进入+(BOOL)resolveInstanceMethod:(SEL)sel
这个方法中,在这里你可以通过判断方法的名字(字符串)来判断要处理的方法。然后通过class_addMethod
来为这个方法动态绑定一个实现方法。这个实现方法是一个标准的C调用方法,如例子中的:void IMPTestMethod1Funcation(id self,SEL _cmd,NSString *name,NSString *nick)
其中前两个参数是必须的:
第一个id self
表示方法执行的对象,
第二个SEL _cmd
表示这个C方法要替代那个OC方法方法的SEL。
后面就是被替代OC方法方法要根的参数(我这个OC方法有两个参数,所以后面跟上两个参数)
打印结果:
2016-10-31 15:30:06.205 ObRunTime[35215:6061873] 调用的方法是什么:testMethod1
2016-10-31 15:30:06.205 ObRunTime[35215:6061873] 调用的方法是什么:_dynamicContextEvaluation:patternString:
2016-10-31 15:30:06.206 ObRunTime[35215:6061873] 调用的方法是什么:descriptionWithLocale:
2016-10-31 15:30:06.206 ObRunTime[35215:6061873] IMP C 函数中实现:<Person: 0x600000024440>, testMethod1
2016-10-31 15:30:06.206 ObRunTime[35215:6061873] 我带入的参数是name:小胡 nick:酒鬼
关于
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
第四个参数*types,解释一下,比较让人晕,这里摸索后解释一下:
我这里用的是@:@@
,其实完整写法是v@:@@
,其中v@:
表示返回值是void(没有返回值),后面的两个@
分别表示这个方法传入了两个参数。如果你只传了一个参数就用v@:@
就可以。还有i@:
表示能有一个int返回值,后面参数跟几个同理了。
当然如果你觉的class_addMethod中添加的是IMP是一个C函数看的不习惯的话,可以这样写:
class_addMethod(self.class, sel, class_getMethodImplementation(self, @selector(你的oc方法)), "v@:@@");
//替换C函数的OC函数
-(void)test:(NSString *)agr1 agr2:(NSString *)agr2{
NSLog(@"我带入的参数是name:%@ nick:%@ ",agr1,agr2);
NSLog(@"要做什么事TODO");
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSString *selString = NSStringFromSelector(sel);
NSLog(@"调用的方法是什么:%@",selString);
if ([selString isEqualToString:@"testMethod1"]) {
// class_addMethod(self.class, sel, (IMP)IMPTestMethod1Funcation, "@:@@");
class_addMethod(self.class, sel, class_getMethodImplementation(self, @selector(test:agr2:)), "v@:@@");
}
return [super resolveInstanceMethod:sel];
}
打印结果:
2016-10-31 16:03:46.543 ObRunTime[35350:6168967] 调用的方法是什么:testMethod1
2016-10-31 16:03:46.543 ObRunTime[35350:6168967] 我带入的参数是name:小胡 nick:酒鬼
2016-10-31 16:03:46.544 ObRunTime[35350:6168967] 要做什么事TODO
2>,备用接收者
如果在上一步无法处理消息,则Runtime会继续调以下方法:
-(id)forwardingTargetForSelector:(SEL)aSelector
如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。
如下我申明了一个ObjectHelper
类
#import "ObjectHelper.h"
@implementation ObjectHelper
-(void)testMethodHelp:(NSString *)string{
NSLog(@"方法转移到这个类来实现了 参数:%@",string);
}
@end
现在我�直接调用Person的这个方法,因为Person没有这个方法,我把它转发给ObjectHelper
Person *testPerson = [[Person alloc]init];
[testPerson performSelector:@selector(testMethodHelp:) withObject:@"转发测试"];
/***********消息转发给别的类处理************/
//如果resolveInstanceMethod 无法处理消息 就会给你一次机会 让你把这个方法分发的别的对象去处理
-(id)forwardingTargetForSelector:(SEL)aSelector{
NSString *selString = NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"testMethodHelp:"]) {
return [[ObjectHelper alloc]init];
}
return [super forwardingTargetForSelector:aSelector];
}
控制台打印:
2016-10-31 17:43:20.447 ObRunTime[35692:6278785] 调用的方法是什么:testMethodHelp:
2016-10-31 17:43:20.447 ObRunTime[35692:6278785] 不是当前要研究的方法
2016-10-31 17:43:20.447 ObRunTime[35692:6278785] 方法转移到这个类来实现了 参数:转发测试
3>,完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象(不止一个对象,可以是多个)。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。
forwardInvocation:方法的实现有两个任务:
- 定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。
- 使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。
不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。
还有一个很重要的问题,我们必须重写以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。
其实签名,可以理解找到一个实现该方法的类,然后把这个方法的名称、参数、返回值包装起来,打一个包!
下面举一个例子,还是我的Person
类,我在ViewController
调用了一个他没有实现的方法testMethodInvork
/*******消息完全转发***********/
Person *testPerson = [[Person alloc]init];
[testPerson performSelector:@selector(testMethodInvork)];
我们把消息分发给ObjectHelper
处理,ObjectHelper
实现testMethodInvork
方法
-(void)testMethodInvork{
NSLog(@"天啊,消息转发了");
}
Person
中的实现:
/**********处理未知消息的步骤**********/
//C函数中IMP其实就是标准的 C函数调用
void IMPTestMethod1Funcation(id self,SEL _cmd,NSString *name,NSString *nick){
NSLog(@"IMP C 函数中实现:%@, %@", self, NSStringFromSelector(_cmd));
NSLog(@"我带入的参数是name:%@ nick:%@ ",name,nick);
}
//替换C函数的OC函数
-(void)test:(NSString *)agr1 agr2:(NSString *)agr2{
NSLog(@"我带入的参数是name:%@ nick:%@ ",agr1,agr2);
NSLog(@"要做什么事TODO");
}
/***********1,动态方法解析(运行的时候添加实现方法)************/
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"第一步:处理未知消息");
NSString *selString = NSStringFromSelector(sel);
NSLog(@"调用的方法是什么:%@",selString);
if ([selString isEqualToString:@"testMethod1"]) {
// class_addMethod(self.class, sel, (IMP)IMPTestMethod1Funcation, "@:@@");
class_addMethod(self.class, sel, class_getMethodImplementation(self, @selector(test:agr2:)), "v@:@@");
}else{
NSLog(@"第一步中不是当前要研究的方法");
}
return [super resolveInstanceMethod:sel];
}
/***********2,消息转发给别的类处理(如果1中途不对方法进行处理)************/
//如果resolveInstanceMethod 无法处理消息 就会给你一次机会 让你把这个方法分发的别的对象去处理
-(id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"第二步:转发消息给别的类实现");
NSString *selString = NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"testMethodHelp:"]) {
return [[ObjectHelper alloc]init];
}else{
NSLog(@"第二步没有转发消息");
}
return [super forwardingTargetForSelector:aSelector];
}
/***********3,完整的消息转发 (1 和 2 都不处理这个消息,就把这个消息封装出来,实现完整转发)************/
-(void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"第三步:最后的完全转发");
if([ObjectHelper instancesRespondToSelector:anInvocation.selector]){
ObjectHelper *helper = [[ObjectHelper alloc]init];
[anInvocation invokeWithTarget:helper];
}
}
//签名这个方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if([ObjectHelper instancesRespondToSelector:aSelector]){
signature = [ObjectHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
运行结果:
2016-11-02 11:26:47.638 ObRunTime[41985:8436765] 第一步:处理未知消息
2016-11-02 11:26:47.638 ObRunTime[41985:8436765] 调用的方法是什么:testMethodInvork
2016-11-02 11:26:47.638 ObRunTime[41985:8436765] 第一步中不是当前要研究的方法
2016-11-02 11:26:47.639 ObRunTime[41985:8436765] 第二步:转发消息给别的类实现
2016-11-02 11:26:47.639 ObRunTime[41985:8436765] 第二步没有转发消息
2016-11-02 11:26:47.639 ObRunTime[41985:8436765] 第一步:处理未知消息
2016-11-02 11:26:47.639 ObRunTime[41985:8436765] 调用的方法是什么:testMethodInvork
2016-11-02 11:26:47.640 ObRunTime[41985:8436765] 第一步中不是当前要研究的方法
2016-11-02 11:26:47.640 ObRunTime[41985:8436765] 第三步:最后的完全转发
2016-11-02 11:26:47.640 ObRunTime[41985:8436765] 天啊,消息转发了
上面这个例子能很清楚的看到,消息转发的三个步骤,是不是很爽,哈哈!
下面举一个实际的开发场景,大家可以看到这篇博客:http://kittenyang.com/forwardinvocation/
问题描述,看博客就行,简单的一句话就是,UIScorllView的委托Delegate,能不能让两个或更多的实例同时相应!
我们就可以用到消息转发机制,让多个对象同时响应Delegate ,为此我们创建一个Delegate分发类DelegateRouter.h
,让它响应目标delegate,因为它没有实现delegate,所以通过消息转发 分发给目标1:ViewController
和 目标2:SecondDelegate
DelegateRouter.h
文件如下:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "ViewController.h"
#import "SecondDelegate.h"
@interface DelegateRouter : NSObject
@property (weak,nonatomic) ViewController *vcDelegate;
@property (strong,nonatomic) SecondDelegate *secDelegate;
@end
DelegateRouter.m
文件如下:
#import "DelegateRouter.h"
@implementation DelegateRouter
-(BOOL)respondsToSelector:(SEL)aSelector{
// NSString *test = NSStringFromSelector(aSelector);
// NSLog(@"响应方法:%@",test);
if ([self.vcDelegate respondsToSelector:aSelector] ||[self.secDelegate respondsToSelector:aSelector]) {
return YES;
}else{
return NO;
}
}
//+(BOOL)resolveInstanceMethod:(SEL)sel{
// return [super resolveInstanceMethod:sel];
//}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
if ([self.vcDelegate respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:self.vcDelegate];
}
if ([self.secDelegate respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:self.secDelegate];
}
}
//方法签名就是 配置一段方法字符串和参数的唯一性,不是和类进行绑定
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
NSMethodSignature *firstMethodSignature = [self.vcDelegate methodSignatureForSelector:aSelector];
NSMethodSignature *secondMethodSignature = [self.secDelegate methodSignatureForSelector:aSelector];
if (firstMethodSignature) {
NSLog(@"注册1");
methodSignature = firstMethodSignature;
}else if (secondMethodSignature){
NSLog(@"注册2");
methodSignature = secondMethodSignature;
}
return methodSignature;
}
return methodSignature;
}
@end
这个需要强调:
- 我需要路由类
DelegateRouter.h
响应我需要委托的方法,所以加上判断-(BOOL)respondsToSelector:(SEL)aSelector
,只有这个返回YES 这个对象才响应这个方法,才会调用【object sendmsg】,如果没有实现方法,才会走消息的转发流程。这里我只需要处理ViewController
和SecondDelegate
里的指定委托,所以我加了判断,别的我直接不让它响应,他就无法转发。 -
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
签名方法,不针对类,只要你这个类实现了你要执行的方法,都可以签名这个方法,这里实现加个if~else
其实只会执行注册1
我们的两个响应委托的地方,一个是ViewController
中
#pragma mark - UIScrollViewDelegate
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSLog(@"我在第一出响应delegate");
}
一个是SecondDelegate类中
@implementation SecondDelegate
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSLog(@"我在第二次响应Delegate");
}
我们再给ScrollViewDelegate 赋值的时候就选择赋给'DelegateRouter'
delegateRouter = [[DelegateRouter alloc]init];
delegateRouter.vcDelegate = self;
delegateRouter.secDelegate = [[SecondDelegate alloc]init];
UIScrollView *scrollView = [[UIScrollView alloc]initWithFrame:self.view.bounds];
scrollView.delegate = delegateRouter;
scrollView.backgroundColor = [UIColor orangeColor];
scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.frame), 700);
[self.view addSubview:scrollView];
这里需要注意,你响应的Delegate必须是各全局或者静态变量,不然无法执行Delegate。
执行控制台输出:
2016-11-02 16:08:55.445 ObRunTime[42837:8844820] 注册1
2016-11-02 16:08:55.445 ObRunTime[42837:8844820] 我在第一出响应delegate
2016-11-02 16:08:55.446 ObRunTime[42837:8844820] 我在第二次响应Delegate
2016-11-02 16:08:55.465 ObRunTime[42837:8844820] 注册1
2016-11-02 16:08:55.465 ObRunTime[42837:8844820] 我在第一出响应delegate
2016-11-02 16:08:55.465 ObRunTime[42837:8844820] 我在第二次响应Delegate
2016-11-02 16:08:55.496 ObRunTime[42837:8844820] 注册1
2016-11-02 16:08:55.496 ObRunTime[42837:8844820] 我在第一出响应delegate
2016-11-02 16:08:55.497 ObRunTime[42837:8844820] 我在第二次响应Delegate
2016-11-02 16:08:55.514 ObRunTime[42837:8844820] 注册1
两个类同时响应多个,同理,你也可以弄更多的对象!
以上代码完整版下载:链接: http://pan.baidu.com/s/1kVdOizH 密码: ijtv
博客参照:http://southpeak.github.io/2014/11/03/objective-c-runtime-3/
自己理解,欢迎板砖!!!!哈哈!!!