Runtime 运行时:方法与消息

SEL

SEL又叫方法选择器,表示一个方法的selector的指针,其定义以下:ios

typedef struct objc_selector *SEL;

方法的selector表示运行时方法的名字。Objective-C编译时,会依据每个方法的名字参数序列,生成一个惟一的整型标识 ( Int类型的地址 ),这个标识就是SEL。以下代码所示:git

SEL sel1 = @selector(method1);
NSLog(@"sel : %p", sel1);

输出结果:github

 

两个类之间,无论它们是父类与子类的关系,仍是之间没有这种关系,只要方法名相同,那么方法的SEL就是同样的。每个方法都对应着一个SEL因此在OC同一个类中,不能存在2个同名的方法,即便参数类型不一样也不行。相同的方法只能对应一个SELobjective-c

不一样的类能够拥有相同的selector,这个没有问题。不一样类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找本身对应的IMP缓存

工程中的全部的SEL组成一个Set集合,Set的特色就是惟一,所以SEL是惟一的。数据结构

本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能惟一表明一个方法).架构

 

咱们能够在运行时添加新的selector,也能够在运行时获取已存在的selectorapp

咱们能够经过下面三种方法来获取SEL:框架

  1. sel_registerName函数
  2. Objective-C编译器提供的@selector()
  3. NSSelectorFromString()方法

 

 

IMP

IMP其实是一个指向函数的指针,指向方法实现的首地址。其定义以下:ide

id (*IMP)(id, SEL, ...)

这个函数使用当前CPU架构实现的标准的C调用约定。第一个参数是指向self的指针(若是是对象方法,则是类对象的内存地址;若是是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。

SEL就是为了查找方法的最终实现IMP。因为每一个方法对应惟一的SEL,所以咱们能够经过SEL方便快速准确地得到它所对应的IMP。取得IMP后,咱们就得到了执行这个方法代码的入口点,此时,咱们就能够像调用普通的C语言函数同样来使用这个函数指针了。

经过取得IMP咱们能够跳过Runtime的消息传递机制,直接执行IMP指向的函数实现这样省去了Runtime消息传递过程当中所作的一系列查找操做,会比直接向对象发送消息高效一些。

 

 

Method

Method用于表示类定义中的方法,则定义以下:

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                	OBJC2_UNAVAILABLE;	// 方法名
    char *method_types                	OBJC2_UNAVAILABLE;
    IMP method_imp             			OBJC2_UNAVAILABLE;	// 方法实现
}

该结构体中包含一个SELIMP,实际上至关于在SELIMP之间做了一个映射。有了SEL,咱们即可以找到对应的IMP,从而调用方法的实现代码。

 

 

objc_method_description

objc_method_description定义了一个Objective-C方法,其定义以下:

struct objc_method_description { SEL name; char *types; };

 

 

方法相关操做函数

Runtime提供了一系列的方法  用来处理与方法相关的操做。包括方法自己及SEL。

方法自己

方法操做相关函数:

// 调用指定方法的实现
id method_invoke ( id receiver, Method m, ... );

// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );

// 获取方法名
SEL method_getName ( Method m );

// 返回方法的实现
IMP method_getImplementation ( Method m );

// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );

// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );

// 获取方法的指定位置参数的类型字符串
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 );
  • method_invoke函数,返回的是实际实现的返回值。参数receiver不能为空。这个方法的效率会比method_getImplementationmethod_getName更快。
  • method_getName函数,返回的是一个SEL。若是想获取方法名的C字符串,可使用sel_getName(method_getName(method))
  • method_getReturnType函数,类型字符串会被拷贝到dst中。
  • method_setImplementation函数,注意该函数返回值是方法以前的实现。

 

 

方法选择器

选择器相关的操做函数

// 返回给定选择器指定的方法的名称
const char * sel_getName ( SEL sel );

// 在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
SEL sel_registerName ( const char *str );

// 在Objective-C Runtime系统中注册一个方法
SEL sel_getUid ( const char *str );

// 比较两个选择器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
  • sel_registerName函数:在咱们将一个方法添加到类定义时,咱们必须在Objective-C Runtime系统中注册一个方法名以获取方法的选择器。

 

 

方法调用流程

在OC中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。这个函数将消息接收者方法名做为其基础参数,如如下所示:

objc_msgSend(receiver, selector)

若是消息中还有其余参数

objc_msgSend(receiver, selector, arg1, arg2, ...)

这个函数完成了动态绑定的全部事情:

  1. 首先它找到selector对应的方法实现。由于同一个方法可能在不一样的类中有不一样的实现,因此咱们须要依赖于接收者的类来找到确切的实现。
  2. 它调用 方法实现,并将接收者对象方法的全部参数传给 方法实现。
  3. 最后,它将 实现返回的值 做为它本身的返回值。

消息的关键在于 结构体objc_class,这个结构体有两个字段是咱们在分发消息时要关注的:

  1. 指向父类的指针
  2. 一个类的方法分发表,即methodLists

当咱们建立一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象能够访问类及类的继承体系。

下图演示了这样一个消息的基本框架:

image

 

当消息发送给一个对象时,objc_msgSend经过对象的isa指针获取到类的结构体,而后在方法分发表里面查找方法的selector。若是没有找到selector,则经过类的结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。若是最后没有定位到selector,则会走消息转发流程,这个咱们在后面讨论。

为了加速消息的处理,运行时系统缓存使用过的selector及对应的方法的地址。

 

 

隐藏参数

objc_msgSend有两个隐藏参数:

  1. 消息接收对象
  2. 方法的selector

这两个参数为 方法的实现 提供了调用者的信息。之因此说是隐藏的,是由于它们在定义方法的源代码中没有声明。它们是在编译期被插入实现代码的。

虽然这些参数没有显示声明,但在代码中仍然能够引用它们。可使用 self来引用接收者对象,使用_cmd来引用选择器

 

获取方法地址

若是想要避开这种动态绑定方式,咱们能够获取方法实现的地址,而后像调用函数同样来直接调用它。特别是当咱们须要在一个循环内频繁地调用一个特定的方法时,经过这种方式能够提升程序的性能。

NSObject类提供了methodForSelector:方法,能够获取到方法的指针(指向函数的指针),而后经过这个指针来调用实现代码。咱们须要将methodForSelector:返回的指针转换为合适的函数类型函数参数返回值都须要匹配上。

void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for (i = 0 ; i < 1000 ; i++){
    setter(targetList[i], @selector(setFilled:), YES);
}

注意:函数指针的前两个参数必须是idSEL

固然这种方式只适合于在相似于for循环这种状况下频繁调用同一方法,以提升性能的状况。另外,methodForSelector:是由Cocoa运行时提供的;它不是Objective-C语言的特性。

 

 

消息转发

当一个对象能接收一个消息时,就会走正常的方法调用流程。但若是一个对象没法接收指定消息时,又会发生什么事呢?默认状况下,若是是以[object message]的方式调用方法,若是object没法响应message消息时,编译器会报错。但若是是以perform...的形式来调用,则须要等到运行时才能肯定object是否能接收message消息。若是不能,则程序崩溃。

当咱们不能肯定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。以下代码所示:

if ([self respondsToSelector:@selector(method)]) {
    [self performSelector:@selector(method)];
}

不过,我想讨论下不使用respondsToSelector:判断的状况。

当一个对象没法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,经过这一机制,咱们能够告诉对象如何处理未知的消息。默认状况下,对象接收到未知的消息,会致使程序崩溃,经过控制台,咱们能够看到如下异常信息:

-[SUTRuntimeMethod method]: 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. 完整转发

下面咱们详细讨论一下这三个步骤。

动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:或者+resolveClassMethod:。在这个方法中,咱们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是咱们已经实现了该”处理方法”,只须要在运行时经过class_addMethod函数动态添加到类里面就能够了。以下代码所示:

不过这种方案更多的是为了实现@dynamic属性。

 

备用接收者

若是在上一步没法处理消息,则Runtime会继续调如下方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

若是一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会做为消息的新接收者,且消息会被分发到这个对象。固然这个对象不能是self自身,不然就是出现无限循环。固然,若是咱们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

使用这个方法一般是在对象内部,可能还有一系列其它对象能处理该消息,咱们即可借这些对象来处理消息并返回,这样在对象外部看来,仍是由该对象亲自处理了这一消息。以下代码所示:

@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

这一步合适于咱们只想将消息转发到另外一个能处理该消息的对象上。但这一步没法对消息进行处理,如操做消息的参数和返回值。

 

完整消息转发

http://southpeak.github.io/2014/11/03/objective-c-runtime-3/

相关文章
相关标签/搜索