美文网首页iOS Developer
Runtime系列二:Runtime的原理

Runtime系列二:Runtime的原理

作者: 小霍同学 | 来源:发表于2017-05-28 16:54 被阅读57次

    前言:

    关于Runtime的资料网上一搜很多,但总是写的只言片语,不太全面。最近花了一个星期的时间重新学习Runtime,并整理了一个系列文章,并发表出来,同时也感谢开源贡献的开发者。这里共有三篇文章:

    Runtime系列一:Runtime的前世今生

    Runtime系列二:Runtime的原理

    Runtime系列三:Runtime在项目中使用场景

    一、Runtime的几个概念

    1.1、SEL

    SEL又叫方法选择器,这到底是个什么玩意呢?在objc.h中是这样定义的:

    typedef  struct  objc_selector *SEL;

    这个SEL表示什么?首先,说白了,方法选择器仅仅是一个char *指针,仅仅表示它所代表的方法名字罢了。Objective-C在编译的时候,会根据方法的名字,生成一个用 来区分这个方法的唯一的一个ID,这个ID就是SEL类型的。我们需要注意的是,只要方法的名字相同,那么它们的ID都是相同的。就是说,不管是超类还是子类,不管是有没有超类和子类的关系,只要名字相同那么ID就是一样的。

    而这也就导致了Objective-C在处理有相同函数名和参数个数但参数类型不同的函数的能力非常的弱,比如当你想在程序中实现下面两个方法:

    -(void)setWidth:(int)width;

    -(void)setWidth:(double)width;

    这样的函数则被认为是一种编译错误,而这最终导致了一个非常非常奇怪的Objective-C特色的函数命名:

    -(void)setWidthIntValue:(int)width;

    -(void)setWidthDoubleValue:(double)width;

    可能有人会问,runtime费了那么老半天劲,究竟想做什么?

    刚才我们说道,编译器会根据每个方法的方法名为那个方法生成唯一的SEL,这些SEL组成了一个Set集合,这个Set简单的说就是一个经过了优化过的hash表。而Set的特点就是唯一,也就是SEL是唯一的,因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,犀利,速度上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么SEL仅仅是函数名了。

    到这里,我们明白了,本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度!!!!

    通过下面三种方法可以获取SEL:

    a、sel_registerName函数

    b、Objective-C编译器提供的@selector()

    c、NSSelectorFromString()方法

    1.2、IMP,方法实现的指针

    IMP在objc.h中是如此定义的:

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

    第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),这个比SEL要好理解多了,熟悉C语言的同学都知道,这其实是一个函数指针。

    第二个参数:是方法选择器(selector)

    接下来的参数:方法的参数列表。

    前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。

    下面的例子,介绍了取得函数指针,即函数指针的用法:

    void(* performMessage)(id,SEL);//定义一个IMP(函数指针)

    performMessage = (void(*)(id,SEL))[self  methodForSelector:@selector(message)];//通过methodForSelector方法根据SEL获取对应的函数指针

    performMessage(self,@selector(message));//通过取到的IMP(函数指针)跳过runtime消息传递机制,直接执行message方法

    用IMP 的方式,省去了runtime消息传递过程中所做的一系列动作,比直接向对象发送消息高效一些。

    1.3、Method

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

    typedef struct objc_method *Methodstructobjc_method{

    SEL method_name      OBJC2_UNAVAILABLE; // 方法名

    char *method_types  OBJC2_UNAVAILABLE;

    IMP method_imp      OBJC2_UNAVAILABLE; // 方法实现

    }

    我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码

    1.4、元类(Meta Class)

    meta-class是一个类对象的类(注意是类对象)。

    在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。

    既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么,这个isa指针指向什么呢?

    为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,meta-class中存储着一个类的所有类方法。

    所以,调用类方法的这个类对象的isa指针指向的就是meta-class

    当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

    再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。

    即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。

    通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了,如下代码

    meta_class


    1.5、Category

    Category是表示一个指向分类的结构体的指针,其定义如下:

    typedef struct objc_category*Category {

    char*category_name                        OBJC2_UNAVAILABLE;// 分类名char*class_name                            OBJC2_UNAVAILABLE;// 分类所属的类名structobjc_method_list*instance_methods  OBJC2_UNAVAILABLE;// 实例方法列表structobjc_method_list*class_methods      OBJC2_UNAVAILABLE;// 类方法列表structobjc_protocol_list*protocols        OBJC2_UNAVAILABLE;// 分类所实现的协议列表}

    这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。

    可发现,类别中没有ivar成员变量指针,也就意味着:类别中不能够添加实例变量和属性

    structobjc_ivar_list*ivars            OBJC2_UNAVAILABLE;// 该类的成员变量链表

    1.6、objc_class

    Objective-C类是由Class类型来表示的,它实际上是一个指

    向objc_class结构体的指针。

    typedef struct object_class *Class

    它的定义如下:

    查看objc/runtime.h中objc_class结构体的定义如下:

    struct object_class{    Class isa OBJC_ISA_AVAILABILITY;

    #if!__OBJC2__Class super_class

    OBJC2_UNAVAILABLE;// 父类constchar*name                        OBJC2_UNAVAILABLE;// 类名longversion

    OBJC2_UNAVAILABLE;// 类的版本信息,默认为0longinfo                                OBJC2_UNAVAILABLE;// 类信息,供运行期使用的一些位标识longinstance_size                      OBJC2_UNAVAILABLE;// 该类的实例变量大小structobjc_ivar_list *ivars            OBJC2_UNAVAILABLE;// 该类的成员变量链表structobjc_method_list *methodLists    OBJC2_UNAVAILABLE;// 方法定义的链表structobjc_cache *cache                OBJC2_UNAVAILABLE;// 方法缓存structobjc_protocol_list *protocols    OBJC2_UNAVAILABLE;// 协议链表#endif}OBJC2_UNAVAILABLE;

    1.7、objc_object

    objc_object是表示一个类的实例的结构体

    它的定义如下(objc/objc.h):

    struct objc_object{    Class isa OBJC_ISA_AVAILABILITY;};

    typedef  struct objc_object*id;

    可以看到,这个结构体只有指向其类的isa指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法,找到后即运行这个方法。

    二、消息调用流程

    2.1、传递消息所用的几个runtime方法

    上篇文章中我们说过,下面的方法:

    [receiver message]

    objc_msgSend(receiver, selector)

    实际上,同objc_msgSend方法类似的还有几个:

    objc_msgSend_stret(返回值是结构体)

    objc_msgSend_fpret(返回值是浮点型)

    objc_msgSendSuper(调用父类方法)

    objc_msgSendSuper_stret(调用父类方法,返回值是结构体)

    它们的作用都是类似的,为了简单起见,后续介绍消息和消息传递机制都以objc_msgSend方法为例。

    2.2、消息调用

    一切还是从消息表达式[receiver message]开始,在被转换成objc_msgSend(receiver, SEL)后,在运行时,runtime system会做以下事情:

    1、检查忽略的Selector,比如当我们运行在有垃圾回收机制的环境中,将会忽略retain和release消息。

    2、检查receiver是否为nil。不像其他语言,nil在objective-C中是完全合法的,并且这里有很多原因你也愿意这样,比如,至少我们省去了给一个对象发送消息前检查对象是否为空的操作。如果receiver为空,则会将 selector也设置为空,并且直接返回到消息调用的地方。如果对象非空,就继续下一步。

    3、接下来会根据SEL到当前类中查找对应的IMP,首先会在cache中检索它,如果找到了就根据函数指针跳转到这个函数执行,否则进行下一步。

    4、检索当前类对象中的方法表(method list),如果找到了,加入cache中,并且就跳转到这个函数之行,否则进行下一步。

    5、从父类中寻找,直到根类:NSObject类。找到了就将方法加入对应类的cache表中,如果仍为找到,则要进入后文介绍的内容:动态方法决议

    6、如果动态方法决议仍不能解决问题,只能进行最后一次尝试,进入消息转发流程

    7、如果还不行,会崩溃。

    这里的调用可以分成两部分

    2.2.1、调用的方法可以找到(执行步骤1-4)

    下面的图部分展示了这个调用过程:

    当消息发送给一个对象时首先从运行时系统缓存使用过的方法中寻找。

    如果找到,执行该方法,如未找到继续执行下面的步骤

    objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。

    如果没有找到selector,objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。

    依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现,并将该方法添加进入缓存中如果最后没有定位到selector,则会走动态解析流程。

    调用流程-1

    2.2.2、调用的方法找不到(消息转发机制)

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

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

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

    不过,我们这边想讨论下不使用respondsToSelector:判断的情况。这才是我们这一节的重点。

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

    这段异常信息实际上是由NSObject的“doesNotRecognizeSelector”方法抛出的。不过,我们可以采取一些措施,在程序崩溃前执行特定的逻辑,而避免程序的崩溃。

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

    <1>、动态方法解析

    <2>、备用接收者

    <3>、完整转发

    消息的转发流程图:

    消息转发-2

    <1>、动态方法解析

    对象在接收到未知的消息时,首先会调用所属类的类方法

    +resolveInstanceMethod:(实例方法)或者

    +resolveClassMethod:(类方法)。

    让我们可以在程序运行时动态的为一个selector提供实现,如果我们添加了函数的实现,并返回YES,运行时系统会重启一次消息的发送过程,调用动态添加的方法。例如,下面的例子:

    + (BOOL)resolveInstanceMethod:(SEL)sel{

    if(sel == @selector(foo)) {

    class_addMethod([self class], sel, (IMP)dynamicMethodIMP,"V@:");

    returnYES;

    }return[superresolveInstanceMethod:sel];

    }

    void dynamicMethodIMP(id self, SEL _cmd){

    NSLog(@"%s", __PRETTY_FUNCTION__);

    }

    在这个方法中,我们有机会为该未知消息新增一个“处理方法”,通过运行时class_addMethod函数动态添加到类里面就可以了。

    这种方案更多的是为了实现@dynamic属性。注:@dynamic关键字就是告诉编译器不要做这些事,同时在使用了存取方法时也不要报错,即让编译器相信存取方法会在运行时找到。

    <2>、备用接收者

    -(id)forwardingTargetForSelector:(SEL)aSelector

    如果在上一步无法处理消息,则Runtime会继续调以下方法:

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

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

    <3>、完整消息转发

    如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。

    我们首先要通过,指定方法签名,若返回nil,则表示不处理。

    如下代码:

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{

    if([NSStringFromSelector(aSelector)isEqualToString:@"testInstanceMethod"]){

    return[NSMethodSignaturesignatureWithObjcTypes:"v@:"];  }

    return[supermethodSignatureForSelector:aSelector];

    }

    若返回方法签名,则会进入下一步调用以下方法,对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。

    我们可以在forwardInvocation方法中选择将消息转发给其它对象。我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等.

    - (void)forwardInvovation:(NSInvocation)anInvocation{   

    [anInvocationinvokeWithTarget:_helper]; 

      [anInvocationsetSelector:@selector(run)]; 

      [anInvocationinvokeWithTarget:self];

    }

    三、函数检索优化措施

    写到这大家肯定会发出这样的疑问:我仅仅想调用一个方法而已,却不得不经历那么多步骤,效率上怎么保证??苹果也做了一些优化上的工作。主要从下面两个方面着手:

    3.1、通过SEL进行IMP匹配

    先来看看类对象中保存的方法列表和方法的数据结构

    typedef  structmethod_list_t {

    uint32_t entsize_NEVER_USE;

    uint32_t count;

    struct  method_t first;

    } method_list_t;

    typedef  struct method_t {

    SEL name;

    constchar*types;//参数类型和返回值类型

    IMP imp;

    } method_t;

    在前一篇文章介绍SEL的时候,我们已经说过了苹果在通过SEL检索IMP时做的努力,这里不再累述。

    3.2、cache缓存

    cache的原则就是缓存那些可能要执行的函数地址,那么下次调用的时候,速度就可以快速很多。这个和CPU的各种缓存原理相通。好吧,说了这么多了,再来认识几个名词:

    struct  objc_cache {

    uintptr_tmask;

    uintptr_toccupied;

    cache_entry *buckets[1];

    };

    typedef  struct{

    SEL name;

    void*unused;

    IMP imp;

    } cache_entry;

    看这个结构,有没有搞错又是hash table。

    objc_msgSend 首先在cache list 中找SEL,没有找到就在class method中找,super class method中找(当然super class 也有cache list)。而cache的机制则非常复杂了,由于Objective-C是动态语言。所以,这里面还有很多的多线程同步问题,而这些锁又是效率的大敌,相关的内容已经远远超过本文讨论的范围。

    如果在缓存中已经有了需要的方法选标,则消息仅仅比函数调用慢一点点。如果程序运行了足够长的时间,几乎每个消息都能在缓存中找到方法实现。程序运行时,缓存也将随着新的消息的增加而增加。据牛人说(没有亲测过),苹果通过这些优化,使消息传递和直接的函数调用效率上的差距已经相当的小。

    四、方法调用中的隐藏参数

    在进行面向对象编程的时候,在实例方法中都是用过self关键字吧,可是你有没有想过,为什么在一个实例方法中,通过self关键字就能取到调用当前方法的对象呢?这就要归功与runtime system消息的隐藏参数了。

    当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

     接收消息的对象(也就是self指向的内容)

     方法选标(_cmd指向的内容)

    这些参数帮助方法实现获得了消息表达式的信息。它们被认为是”隐藏“的是因为它们并没有在定义方法的源代码中声明,而是在代码编译时是插入方法的实现中的。尽管这些参数没有被显示声明,但在源代码中仍然可以引用它们(就象可以引用消息接收者对象的实例变 量一样)。在方法中可以通过 self 来引用消息接收者对象,通过选标_cmd 来引用方法本身。下面的例子很好的说明了这个问题:

    - (void)message

    {

    self.name = @"James";//通过self关键字给当前对象的属性赋值

    SEL currentSel = _cmd;//通过_cmd关键字取到当前函数对应的SEL

    NSLog(@"currentSel is :%s",(char*)currentSel);

    }

    打印结果:

    ObjcRunTime[693:403] currentSel is :message

    当然,在这两个参数中,self 更有用,更常用一些。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。


    相关文章

      网友评论

        本文标题:Runtime系列二:Runtime的原理

        本文链接:https://www.haomeiwen.com/subject/kezcfxtx.html