博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
runtime第三部分方法和消息
阅读量:5893 次
发布时间:2019-06-19

本文共 10137 字,大约阅读时间需要 33 分钟。

接上一篇hl

转载来源 

方法和消息  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有两个隐藏参数:

  1. 消息接收对象

  2. 方法的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调用一个方法时,对象无法接受消息,就会启动消息转发机制,在程序崩溃前,我们有三次机会通过消息阻止程序崩溃。

消息转发机制基本上分为三个步骤:

  1. 动态方法解析

  2. 备用接收者

  3. 完整转发

 

对象接受未知消息--调用所属类的的类方法+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 
@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
@implementation ForwardingTarget- (void)sel{ //第二被转移到本类中之后会查找本类中是否有这个方法,这个类有就执行相应的方法 NSLog(@"ForwardingTarget");}- (void)forwardInvocation:(NSInvocation *)anInvocation { //3.进入这个方法中执行相应的方法 [self performSelector:anInvocation.selector withObject:nil];// [super forwardInvocation:anInvocation];}@end

 

消息转发可以达到类似多继承的效果,处理方法二和三,可以允许一个对象与其他对象建立关系,处理某些未知的消息。但是还是有一些区别,例如有的方法不能够用于转发链respondsToSelector:和isKindOfClass:

如果想让这种消息也看起来像继承,也可以重写这些方法。

 

小姐:实际开发中很少用到这些机制,但是是有助于我们更多的去了解底层的实现,实际编码中也可以更灵活的使用这些机制,实现一些特殊的功能,如hook操作等。

 

转载于:https://www.cnblogs.com/ddavidXu/p/5924597.html

你可能感兴趣的文章
nagios搭建(五):nagios监控mysql
查看>>
AIX ftp 530 User root access denied
查看>>
【Java记录】try-with-resources的一个坑
查看>>
如何学习Linux命令-初级篇
查看>>
从Oracle Public Yum为Oracle Linux建立本地的Yum源
查看>>
spring4+mybaits3整合—项目Demo
查看>>
PHP数据类型
查看>>
MySQL utf8mb4 字符集:支持 emoji 表情符号
查看>>
IOS开发-如何debug及处理闪退,My App Crashed,Now What? - P...
查看>>
马云_拥抱敏捷-----华为的SDN实践
查看>>
Cocos2d-x 3移动游戏编程
查看>>
Android开发——09Google I/O之让Android UI性能更高效(1)
查看>>
广度优先搜索知识总结
查看>>
Java多线程机制详解(转)
查看>>
在 SELECT 查询中使用表表达式
查看>>
我的友情链接
查看>>
(二) php if语句,switch语句,continue语句,return语句,for 、while、do while 循环
查看>>
Hadoop集群(第7期)_Eclipse开发环境设置
查看>>
ARC 下两种释放对象的方法
查看>>
scala中的continue和break
查看>>