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有两个隐藏参数:

  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 <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操作等。

 

posted @ 2016-10-08 17:48  ddavidXu  阅读(346)  评论(0编辑  收藏  举报