Runtime总结

作者: 杨千嬅染了红头发 | 来源:发表于2016-12-24 11:39 被阅读196次

    Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放在运行时来处理,这种动态语言的优势在于:我们写代码时更具有灵活性,如我们可以把消息转发给我们想要的对象或随意交换一个方法的实现。

    这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于Objective-C来说。这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行,这个运行时系统即Objc RuntimeObjc Runtime其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。

    Runtime库主要做下面几件事:
    1.封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外加一些额外的特性。这些结构体和函数被runtime函数封装后,我们可以在程序运行时创建,检查,修改类对象和它们的方法了。
    2.找出方法的最终执行代码:当程序执行[object dosomething]时,会向消息接受者(object)发送一条信息(doSomething),RunTime会根据消息接受者是否能响应消息做出不同的反应。

    Class

    Objective-C类是由Class类表示的,它实际是一个指向objc_class结构体的指针,它的定义如下:

    typedef struct objc_class *Class;
    

    objc/runtime.hobjc_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:在Object-C中,所有的类的自身也是一个对象,类和类的实例没有任何本质上的区别,任何对象都有isa的指针。isa是一个Class类型的指针,每个实例对象都有一个isa的指针,他指向对象的类,而类(Class)里也有个isa的指针,指向meteClass(元类)。
    2.super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject)则super_class为NULL。
    3.char *name: 类名。
    4.version:我们可以使用这个字段来提供类的版本信息。
    5.info运行期使用的一些位标识。
    6.instance_size: 该类的实例变量大小。
    7.ivars:objc_ivar_list结构体存储着objc_ivar成员变量数组列表,而'obj_ivar'结构体存储了类的单个成员变量的信息。

    objec_class中,所有得到成员变量,属性是放在链表ivars中的。ivars是一个数组,数组中每个元素都指向Ivar(变量信息)的指针。

    objc_ivar_list *ivars

    struct objc_ivar_list {
    
    int ivar_count OBJC2_UNAVAILABLE;
    
    ifdef LP64int space OBJC2_UNAVAILABLE;
    
    endif/* variable length structure */
    
    struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
    
    } OBJC2_UNAVAILABLE;
    

    Ivar是表示实例变量的类型,其实际是一个指向objc_ivar结构体的指针,其定义如下:

    typedef struct objc_ivar *Ivar;
    
    struct objc_ivar {
    
    char *ivar_name     OBJC2_UNAVAILABLE; // 变量名
    char *ivar_type     OBJC2_UNAVAILABLE; // 变量类型
    int ivar_offset     OBJC2_UNAVAILABLE; // 基地址偏移字节
    
    #ifdef __LP64__
    int space       OBJC2_UNAVAILABLE;
    #endif
    }
    

    从上面可以看出类的实例变量和属性经过runtime经过struct的储存形式存在,并且单个实例变量保存其名字、类型、偏移量和储存空间。类中所有实例变量是以list类型进行储存。

    8.objc_method_listObj_method方法列表。
    9.cache:用于缓存最近使用的方法,一个接收者对象收到一个消息时,会根据isa指针去查找能够响应这个消息的对象,但是在实际使用中,这个对象只有一部分方法是常用的,很多方法很少或者根本用不上,这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差,这时cache就有用了,在我们每次调用 一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候 runtime就会优先去cache中找,如果cache没有,才会去methodLists中查找方法。

    objc_object 与 id

    objc_object是表示一个类的实例的结构体,它的定义如下(objc/objc.h):

    struct objc_object {
    Class isa OBJC_ISA_AVAILABILITY;
    };
    
    typedef struct objc_object *id;
    

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

    当创建一个特定类的实例 对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据,NSObject类的allocallocWithZone方法使用函数class_createInstance来创建objc_object数据结构。

    id,它实际上是一个objc_object结构类型的指针。该类型的对象可以转换为任何一种对象。
    id类型是动态类型的,即运行时再决定对象的类型。id类型即通用的对象类,任何对象都可以被id指针所指,而在实际使用中,往往使用introspection来确定该对象的实际所属类:

    id obj = someInstance;
    if ([obj isKindOfClass:someClass])
    {
        someClass *classSpecifiedInstance = (someClass *)obj;
        // Do Something to classSpecifiedInstance which now is an instance of someClass
        //...
    }
    

    元类(Meal Class)

    所有的类的自身也是一个对象,我们可以向这个对象发送消息(即调用类方法),如:

    NSDictionary *dictionary = [NSDictionary dictionary];
    

    +dictionary消息发送给了NSDictionary类,而这个NSDictionary也是一个
    对象,既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。为了调用+dictionary方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念

    met-class是一个类对象的类。
    

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

    meta-class它存储着一个类的所有类方法。每个类都会有一个单独的meta-class,每个类的类方法基本不可能完全相同。

    meta-class也是一个,也可以向它发送一个消息,那么它的isa又是指向哪里,为了不让这种结构无限延伸下去,Object-C的设计者让所有的meta-classisa指向基类的meta-class,以此作为它们的所属类。即任何NSObject继承体系下的meta-class都会使用NSObject的meta-class作为自己所属类,而基类的isa指针是指向它自己。这样就形成了一个完美的闭环

    消息处理

    SEL

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

    typedef struct objc_selector *SEL;
    

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

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

    上面的输出为:

    2016-11-28 14:04:41.151 RuntimeDemo[89015:1134944] sel : 0x103076a22**
    

    两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么SEL就是一样的,每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这就导致Objecttive-C在处理相同方法名字且参数个数相同但是参数类型不同的方法方面的能力很差。如:

    - (void)dealTotalPriceWithProductCount:(int)productCount;
    
    - (void)dealTotalPriceWithProductCount:(NSUInteger)productCount;
    

    这种定义会被编译器认为是一种编译错误。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP

    在一个工程中所有的SEL组成一个Set集合,Set 的特点是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合 中查找某个方法时,只要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了一个字符串,而对字符串的比较仅仅需要比较它们的地址就可以了,速度上是非常快的。
    本质上,SEL只是一个指向方法的指针(准确说,只是一个方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。

    我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,有三种方法来获取SEL:
    1.sel_registerName函数
    2.Objective-C编译器提供的@selector()
    3.nSSelectorFromString()方法

    IMP

    IMP实际上是一个函数指针,指向方法实现的首地址。它是一个函数指针,由编译器生成,SEL就是为了查找方法的最终实现IMP。每个方法对应唯一的SEL,因此我们可以快速准确地获得它对应的IMP,取得IMP后,我们就获得了执行这个方法代码的入口点,通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样就省去了Runtime消息传递过程中的一系列查找操作,会比直接向对象发送消息高效一些。

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

    第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。

    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; // 方法实现
    }
    

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

    SEL:代表方法名类型,在不同的类中定义,它们的方法选择器也不一样。
    method_types : method_types方法类型是一个char指针,存储着方法的参数类型和返回类型。
    method_imp指向了方法的实现,本质上是一个函数指针。

    方法调用流程

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

    objc_msgSend(receiver, selector)
    

    如果消息中还有其它参数,则该方法的形式如下所示:

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

    这个函数完成了动态绑定的所有事情:
    1..首先找到selector`对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖接受者的类来找到确切的实现。
    2.它调用方法实现,并将接受者对象及方法的所有参数传给它。
    3.最后,它将实现返回的值作为自己的返回值。

    消息的关键在于前面有解释的结构体objc_class,这个结构体有连个字段是我们在分发消息的时候需要关注的:

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

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


    消息传递

    当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依次,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数就获取到了实现得得入口点,并传入相应的参数来执行方法的具体实现。如果没有定位到selector,为了加速消息的处理,运行时系统缓存使用过的'selector'级对应的方法的地址。

    下面以实例对象调用方法[student speek]为例描述调用的流程:

    1.编译器会把`[student speak]`转化为`objc_msgSend(student, SEL)`,SEL为@selector(speek)。
    
    2.runtime会在student对象对应的Student类的方法缓存列表里查找方法的SEL。
    
    3.如果没有找到,则在Student类的方法分发表查找SEL,类对象由对象isa指针指向,分发列表即methodList。
    
    4.如果没有找到,则在父类(设Student的父类是Person类)的方法分发表里查找方法的SEL(父类由类的superClass指向)。
    
    5.如果没有找到,则沿着继承体系继续找下去,最终到达NSObject类停止。
    
    6.如果在2 、3 、4的其中一步找到,会通过SEL找打对应的IMP,即定位到了方法实现的入口,执行具体实现。
    
    7.如果最后还是没有找到,则会进行消息转发。
    

    获取方法地址
    Runtime中方法的动态绑让我们写代码的时候更具有灵活性,如我们可以消息转发给我们想要的对象,或者随意交换一个方法的实现等动态绑定不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现。而不像函数调用得那么直接。当然方法得当缓存一定程度上解决了这一问题。

    如果想要避开这种动态绑定方式,我们可以获取方法实

    Method Swizzing

    Method swizzling 用于改变一个已经存在的selector的实现,这项技术使得在运行时候改变方法的调用成为可能。例如我们想要在一款iOS app中追踪每一个界面呈现给力用户多少次:可以通过在每个视图控制器的viewDidAppear方法中添加追踪代码来实现,但这样会有大量重复的代码,继承也会有同样的问题,利用method swizzling可以较完美实现:

    #import <objc/runtime.h>
    @implementation UIViewController (Tracking)
    + (void)load { 
    
     static dispatch_once_t onceToken; 
     dispatch_once(&onceToken, ^{ 
     Class class = [self class]; 
    
    //源方法的SEL
     SEL originalSelector = @selector(viewWillAppear:); 
    //交换方法的SEL
     SEL swizzledSelector = @selector(prefix_viewWillAppear:);
    
    /*
    通过class_getInstanceMethod( ) 函数从当前对象中的method list获取method结构体,
    如果类方法那么就使用class_getClassMethod ( ) 函数获取。
    */
     Method originalMethod = class_getInstanceMethod(class, originalSelector); 
     Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    /** 
    * 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
     * 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
     * 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
     */
    
    BOOL didAddMethod = class_addMethod(class, 
                                        originalSelector, 
                                        method_getImplementation(swizzledMethod),  
                                        method_getTypeEncoding(swizzledMethod)
                                        );
    
    
     if (didAddMethod) { 
    
    //添加成功:将源方法的实现替换到交换方法的实现
      class_replaceMethod(class,
                          swizzledSelector, 
                          method_getImplementation(originalMethod), 
                          method_getTypeEncoding(originalMethod)
                          ); 
       } else {
    //添加失败:  说明源方法已经有实现, 直接将两个方法的实现交换即可
         method_exchangeImplementations(originalMethod, swizzledMethod); 
    } 
    });
    
    }
    
    #pragma mark - Method Swizzling
    
    - (void)xxx_viewWillAppear:(BOOL)animated {
     [self xxx_viewWillAppear:animated];
     NSLog(@"viewWillAppear: %@", self);
    }
    
    @end
    

    swizzling应该只在+load中完成。在Object-C的运行时中,每个类都有两个方法自动调用。
    +load是在一个类被初始装载时调用,+initialize是在应用应用第一次调用该类的类方法或实例
    方法前调用。两个方法都是可选的,并且只有在方法被实现的情况下才会被调用。

    ** swizzling 应该只在 dispatch_once 中完成**

    由于 swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatch 的 dispatch_once 满足了所需要的需求,并且应该被当做使用 swizzling 的初始化单例方法的标准。

    相关文章

      网友评论

        本文标题:Runtime总结

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