首页 > 代码库 > runtime第三部分方法和消息
runtime第三部分方法和消息
接上一篇http://www.cnblogs.com/ddavidXu/p/5924049.html
转载来源http://www.jianshu.com/p/6b905584f536
http://southpeak.github.io/2014/10/30/objective-c-runtime-2/
方法和消息 OC中对象调用方法,实际是给对象发送消息
SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
typedef struct objc_selector *SEL;
objc_selector结构体的详细定义没有在头文件中找到。方法的selector用于表示运行时方法的名字
OC在编译时,会依据每一个方法的名字参数序列,生成唯一的整形标识(int类型的地址)这个标识就SEL
SEL sel1 = @selector(method1);NSLog(@"sel : %p", sel1);
2016-09-30 16:50:57.820 XDWRuntimeDemo[3270:264659] sel : 0x106d14bd9
- 两个类之间,不管是不是父子关系,还是没有父子关系,只要方法名字相同,那个方法的SEL就是一样的;
- 每一个方法都对应着一个SEL,所以在OC的同一个类(及继承体系中)中,不能同时存在2个同名的方法,即使参数类型不同也不可以。
- 相同的方法只能对应一个SEL
- 当然,不同的类可以拥有相同的selector,因为不同的实例对象执行相同的selector时,会在各自的方法列表中根据selector去找自己对应的IMP
- 工程中所有的SEL组合成一个set集合,set特点就是唯一性,因此SEL是唯一的。当我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了。
- SEL实际上是根据方法名hash化了一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,速度非常非常的快,有一个问题就是,数量的增多会增大hash冲突而导致性能下降,将总量减少时最犀利的方法,为什么SEL仅仅是函数名。
- 本质上SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的key值,能唯一代表一个方法),它的存在是为了加快方法的查询速度
我们可以通过runtime添加新的selector,也可以通过runtime获取已存在的selector,
sel_registerName函数Objective-C编译器提供的@selector()NSSelectorFromString()方法
IMP
imp实际上是一个函数指针,指向方法的实现首地址
id (*IMP)(id, SEL, ...)
- 这个函数使用当前CPU架构实现的标准的C调用约定。第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。
- 前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的 IMP,取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针 了
通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些.
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,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码
struct objc_method_description { SEL name; char *types; };//方法描述
方法相关操作函数
// 调用指定方法的实现id method_invoke ( id receiver, Method m, ... ); //method_invoke函数,返回的是实际实现的返回值。参数receiver不能为空。这个方法的效率会比method_getImplementation和method_getName更快。// 调用返回一个数据结构的方法的实现void method_invoke_stret ( id receiver, Method m, ... );// 获取方法名SEL method_getName ( Method m ); //method_getName函数,返回的是一个SEL。如果想获取方法名的C字符串,可以使用sel_getName(method_getName(method))// 返回方法的实现IMP method_getImplementation ( Method m );// 获取描述方法参数和返回值类型的字符串const char * method_getTypeEncoding ( Method m );// 获取方法的返回值类型的字符串char * method_copyReturnType ( Method m );//类型字符串会被拷贝到dst中// 获取方法的指定位置参数的类型字符串char * method_copyArgumentType ( Method m, unsigned int index );// 通过引用返回方法的返回值类型字符串void method_getReturnType ( Method m, char *dst, size_t dst_len );// 返回方法的参数的个数unsigned int method_getNumberOfArguments ( Method m );// 通过引用返回方法指定位置参数的类型字符串void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );// 返回指定方法的方法描述结构体struct objc_method_description * method_getDescription ( Method m );// 设置方法的实现IMP method_setImplementation ( Method m, IMP imp );//注意该函数返回值是方法之前的实现// 交换两个方法的实现void method_exchangeImplementations ( Method m1, Method m2 );
方法选择器
// 返回给定选择器指定的方法的名称const char * sel_getName ( SEL sel );// 在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器SEL sel_registerName ( const char *str ); //在我们将一个方法添加到类定义时,我们必须在Objective-C Runtime系统中注册一个方法名以获取方法的选择器// 在Objective-C Runtime系统中注册一个方法SEL sel_getUid ( const char *str );// 比较两个选择器BOOL sel_isEqual ( SEL lhs, SEL rhs );
方法调用流程
在OC中,消息直到运行时才绑定到方法实现上,编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend.这个函数将消息接受者的方法名作为其基础参数。
objc_msgSend(receiver, selector)
objc_msgSend(receiver, selector, arg1, arg2, ...)
这个函数完成了动态绑定的所有事情:
1,首先它找到selector对应的方法实现IMP,因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖接收者的类来找到确切的实现。
2. 它调用方法实现,并将接收者对象及方法的所有参数传给它
3. 最后,它将实现返回的值作为它自己的返回值。
消息的关键在于结构体objc_class
struct objc_class { Class isa OBJC_ISA_AVAILABILITY;#if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;#endif} OBJC2_UNAVAILABLE;
在这个结构体中,我们需要注意
1.指向父类的指针 isa
2.一个类的方法分发表 ,methodlists
创建对象的过程
创建对象-->分配内存-->初始化成员变量(isa指针也会被初始化)
当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系
当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果 没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依 此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实 现。如果最后没有定位到selector,则会走消息转发流程,
消息发送给一个对象-->>objc_msgSend通过对象的isa指针获取到类的结构体-->在方法分发表里面查找方法的selector-->定位到selector,函数会就获取到了实现的入口点,
| 并传入相应的参数来执行方法的具体实现
|
没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector(知道NSObject类)
|
|
如果最后没有定位到selector,则会走消息转发流程,
为了加速消息的处理,运行时系统缓存使用过的selector及对应的方法的地址
隐藏参数
objc_msgSend有两个隐藏参数:
消息接收对象
方法的selector
这两个参数为方法的实现提供了调用者的信息。之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明。它们是在编译期被插入实现代码的。
虽然这些参数没有显示声明,但在代码中仍然可以引用它们。我们可以使用self来引用接收者对象,使用_cmd来引用选择器。如下代码所示:
- strange{ id target = getTheReceiver(); SEL method = getTheMethod(); if ( target == self || method == _cmd ) return nil; return [target performSelector:method];}
获取方法地址
runtime方法中的动态绑定让我们写代码更具有灵活性,比如我们可以把消息转发给我们想要的对象,或者随意交换两个方法的实现等。灵活性的提升也带来了一定的性能耗损,毕竟要查找方法的实现,不像调用函数那么简单,不过方法缓存一定程度上解决了这个问题
MethodForSelector:方法可以获取方法的指针。
IMP str = [self methodForSelector:@selector(setFilled:)];
消息转发
当一个对象调用一个方法时,即给这个对象发送一个消息,假如这个对象无法接受这个消息,即这个对象对应的类,以及对应类的父类中都没有找到这个方法,正常情况下,object无法响应message,编译器会报错,崩溃。
- (void)doesNotRecognizeSelector:(SEL)aSelector { //调用次方法崩溃 [super doesNotRecognizeSelector:aSelector];}
但是如果使用perform的形式来调用方法,会等到运行时才能确定object是否能接收message消息,如果不能,则会崩溃。
(litttle tip)看一个对象是否能响应某个消息时进行检验
if ([self respondsToSelector:@selector(method)]) { [self performSelector:@selector(method)];}
使用perform调用一个方法时,对象无法接受消息,就会启动消息转发机制,在程序崩溃前,我们有三次机会通过消息阻止程序崩溃。
消息转发机制基本上分为三个步骤:
动态方法解析
备用接收者
完整转发
对象接受未知消息--调用所属类的的类方法+resolveInstancheMethod:(实例方法)或者+resolveClassMethod:(类方法)--增加处理方法比如通过classMethod函数动态添
处理方法一(更多的是为了实现@dynamic属性)
void functionForMethod1(id self, SEL _cmd) { NSLog(@"%@, %p", self, _cmd);}+ (BOOL)resolveInstanceMethod:(SEL)sel { NSString *selectorString = NSStringFromSelector(sel); if ([selectorString isEqualToString:@"method1"]) { class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:"); } return [super resolveInstanceMethod:sel];}
处理方法二(当上一种方法中未做处理时,或处理失败,继续调用下面的方法)
- (id)forwardingTargetForSelector:(SEL)aSelector // 如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,这个对象不能是self自身,否则就会出现无线循环,
一般都调用父类的这个方法来实现返回结果
这一步适合我们将消息转发到另一个能处理该消息的对象上,但是无法对消息进行处理
@interface SUTRuntimeMethodHelper : NSObject- (void)method2;@end@implementation SUTRuntimeMethodHelper- (void)method2 { NSLog(@"%@, %p", self, _cmd);}@end#pragma mark -@interface SUTRuntimeMethod () { SUTRuntimeMethodHelper *_helper;}@end@implementation SUTRuntimeMethod+ (instancetype)object { return [[self alloc] init];}- (instancetype)init { self = [super init]; if (self != nil) { _helper = [[SUTRuntimeMethodHelper alloc] init]; } return self;}- (void)test { [self performSelector:@selector(method2)];}- (id)forwardingTargetForSelector:(SEL)aSelector { NSLog(@"forwardingTargetForSelector"); NSString *selectorString = NSStringFromSelector(aSelector); // 将消息转发给_helper来处理 if ([selectorString isEqualToString:@"method2"]) { return _helper; } return [super forwardingTargetForSelector:aSelector];}@end
处理方法三(上一步还是不能处理消息,启动完整的消息转发机制)
- (void)forwardInvocation:(NSInvocation *)anInvocation //NSInvocation对象 尚未处理的消息 有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数
可以实现一些更复杂的功能,内容修改追回参数等,如果发现某个消息不由本类处理,则调用父类的的同名方法,以便继承体系中每个类都有机会处理此调用请求
必须得重写以下方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
栗子
#import "Monkey.h"#import "ForwardingTarget.h"#import <objc/runtime.h>@implementation Monkey- (instancetype)init{ self = [super init]; if (self) { _target = [ForwardingTarget new]; [self performSelector:@selector(sel) withObject:@"yeyuyu"];//第一步,找不到这个方法的实现 } return self;}- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{// //3.第三次机会,调用这个方法,如果返回nil直接崩溃,返回函数签名,则会创建一个对象,执行相应的方法// id result = [super methodSignatureForSelector:aSelector];// NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];// result = sig;// return result; // 3 //第三种情况的第二种写法 NSMethodSignature *sig = [super methodSignatureForSelector:aSelector]; if (!sig) { sig = [ForwardingTarget instanceMethodSignatureForSelector:aSelector]; } return sig; }- (void)forwardInvocation:(NSInvocation *)anInvocation{// //3.返回函数签名后执行这个方法。执行相应的操作// // [super forwardInvocation:anInvocation];// anInvocation.selector = @selector(invocationTest);// [self.target forwardInvocation:anInvocation]; //第三种情况的第二种写法 ForwardingTarget *new = [ForwardingTarget new]; if ([new respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:new]; } }@end#import "ForwardingTarget.h"#import <objc/runtime.h>@implementation ForwardingTarget- (void)sel{ //第二被转移到本类中之后会查找本类中是否有这个方法,这个类有就执行相应的方法 NSLog(@"ForwardingTarget");}- (void)forwardInvocation:(NSInvocation *)anInvocation { //3.进入这个方法中执行相应的方法 [self performSelector:anInvocation.selector withObject:nil];// [super forwardInvocation:anInvocation];}@end
消息转发可以达到类似多继承的效果,处理方法二和三,可以允许一个对象与其他对象建立关系,处理某些未知的消息。但是还是有一些区别,例如有的方法不能够用于转发链respondsToSelector:和isKindOfClass:
如果想让这种消息也看起来像继承,也可以重写这些方法。
小姐:实际开发中很少用到这些机制,但是是有助于我们更多的去了解底层的实现,实际编码中也可以更灵活的使用这些机制,实现一些特殊的功能,如hook操作等。
runtime第三部分方法和消息