美文网首页iOS进阶指南
Objective-C Runtime机制简析

Objective-C Runtime机制简析

作者: 林君毅小号_001 | 来源:发表于2017-05-02 15:41 被阅读140次

    Objective-C在C的基础上添加了面向对象的特性,同时它是一种动态编程语言,将静态语言在编译和链接时需要做的一些事情给延后到运行时执行。例如方法的调用,只有在程序执行的时候,才能具体定位到哪个类的哪个方法。这就需要一个运行时库,就是Runtime。

    1. 类的结构和定义

    在Objective-C中,类实际上是一个objc_class结构体,其定义如下:

    typedef struct objc_class *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;
    
    struct objc_object {
       Class isa  OBJC_ISA_AVAILABILITY;
    };
    
    

    可以看到,在objc2.0中,除了isa指针外,objc_class的其他成员变量皆已被弃用。
    其中isa是objc_class结构体的指针,它指向当前类的meta class。

    • meta class 与 class
      在objc中,class存储类的实例方法(-),meta class存储类的类方法(+),class的isa指针指向meta class。下文会对此详细介绍。

    objc_object结构体就是objc中的对象,它仅包含一个isa指针,指向当前对象所属的类。 我们常用的 id 实质上就是一个objc_object类型的指针。

    图 1.1

    如图1.1所示,一个对象(Instance of Subclass)的isa指针指向它所属的类 Subclass(class),Subclass(class)的isa指针指向 Subclass(meta),Subclass(meta)的isa指针指向Root class(meta)。Root class(meta)的isa指针指向本身。
    同时,Root class(meta)的父类是Root class(class),即NSObject,NSObject的父类为nil。

    2. 方法的调用

    在这里需要先了解几个概念
    SEL
    SEL是objc_selector类型指针,是根据特定规则生成的方法的唯一标识。需要注意的是,只要方法名相同,生成的SEL就相同,与这个方法属于哪个类没有关系。

    typedef struct objc_selector *SEL;
    

    IMP
    如果说,SEL是方法名,那么IMP就是方法的实现。IMP指针定义了一个方法的入口,指向了实现方法的代码块的内存地址。

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

    objc_method
    在objc中,方法实质上是一个objc_method指针。其中,method_name相当于objc_method的hash值,runtime通过method_name找到相应的方法入口(method_imp),从而执行方法的代码块。

    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp                                           OBJC2_UNAVAILABLE;
    }                                                            OBJC2_UNAVAILABLE;
    

    调用一个方法时具体做了什么?
    在Objective-C中,方法的调用采用如下方式:

    [object methodWithArg:arg];
    

    在编译期间,以上代码会被转化为

    objc_msgSend(object, methodWithArg, arg)
    

    可以把它看作是发送消息的过,其中object为消息的接收体,它可能是一个对象,也可能是一个类。若为对象,则是实例方法(- 方法);反之,则是类方法(+方法)。mehodWithArg、arg是具体的消息内容。
    object接收到消息之后,若是实例方法,则会从其所属的类Subclass(class)的methodLists去寻找methodWithArg:方法。若未找着,则到其父类Superclass(class)的methodLists中寻找。以此类推,直到根类NSObject,若仍未找着,就crash。
    同理,若是类方法,则从对象所属类的meta class开始寻找。

    3. 在Objective-C 2.0中的变化

    前面提到过在objc2.0中,objc_class只剩下一个isa指针。由于Xcode对API进行了一定的封装,类的信息并未全部对开发者开放。我们不妨通过阅读Objective-C 2.0的源码去分析,可以通过 官网浏览,或者从github上下载源码。
    从objc-runtime-new.h中可以看到objc_class的定义(只截取关键代码,下文同)

    struct objc_object {
        isa_t isa;
    };
    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() { 
            return bits.data();
        }
    };
    

    其中,superclass指向父类,cache缓存指针、方法入口等,用于提高效率。bits用于存储类名、类版本号、方法列表、协议列表等信息,替代了Objective-C1.0中methodLists、protocols等成员变量。

    class_data_bits_t结构体
    class_data_bits_t结构体中只有一个64位的指针bits,它相当于 class_rw_t 指针加上 rr/alloc 等标志位。其中class_rw_t指针存在于4~47位(从1开始计)。

    图 3.1
    #define FAST_IS_SWIFT         (1UL<<0)
    #define FAST_DATA_MASK        0x00007ffffffffff8UL
    

    is_swift标记位标示是否为swift的类。通过进行位运算可以得到一个class_rw_t类型指针。
    class_rw_t结构体的定义如下

    struct class_rw_t {
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro;
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
    
        Class firstSubclass;
    };
    

    其中methods存储方法列表、properties存储属性列表、protocols存储协议列表。注意到这里有一个class_ro_t类型指针,我们会在下文详细介绍。

    dyld加载镜像
    dyld是objc的动态链接库,在程序运行时,会将镜像加载进内存。

    • 镜像
      工程的编译产物,包括一些动态链接库、Foundation等等,是一些二进制文件。

    在程序初始化方法_objc_init中注册了两个回调

     dyld_register_image_state_change_handler(dyld_image_state_bound,1/*batch*/, &map_2_images);
     dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
    

    其中, map_2_images方法的注释为:Process the given images which are being mapped in by dyld,即处理由dyld映射的给定镜像。它的调用如下:
    map_2_images → map_images_nolock → _read_images → realizeAllClasses
    realizeAllClasses会完成对镜像中所有类的加载和预处理,它最终会调用realizeClass来处理每一个类,而realizeClass又通过调用methodizeClass来对类结构体的methods列表赋值。
    可以通过添加符号断点,来直观的查看这几个方法的调用关系,如图3.2。

    图 3.2

    +load方法
    +load方法会在main方法之前被调用,所有使用到的类的load方法都会被调用。先调用父类的+load方法,再调用子类的+load方法;先调用主类的+load方法,再调用分类的+load方法。

    图 3.3

    图3.3是+load方法的调用栈。load_images 方法是每个镜像加载完毕的回调。

    const char *
    load_images(enum dyld_image_states state, uint32_t infoCount,
                const struct dyld_image_info infoList[])
    {
        bool found;
    
        // Return without taking locks if there are no +load methods here.
        found = false;
        for (uint32_t i = 0; i < infoCount; i++) {
            if (hasLoadMethods((const headerType *)infoList[i].imageLoadAddress)) {
                found = true;
                break;
            }
        }
        if (!found) return nil;
    
        recursive_mutex_locker_t lock(loadMethodLock);
    
        // Discover load methods
        {
            rwlock_writer_t lock2(runtimeLock);
            found = load_images_nolock(state, infoCount, infoList);
        }
    
        // Call +load methods (without runtimeLock - re-entrant)
        if (found) {
            call_load_methods();
        }
    
        return nil;
    }
    

    load_Images会判断镜像是否实现了+load方法,并且调用load_images_nolock方法找到所有+load方法,之后通过call_load_methods调用所有的+load方法。

    class_ro_t
    class_ro_t与class_rw_t的最大区别在于一个是只读的,一个是可读写的,实质上ro就是readonly的简写,rw是readwrite的简写。

    struct class_ro_t {
        const char * name;
        method_list_t * baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;
    };
    

    在编译之后,class_ro_t的baseMethodList就已经确定。当镜像加载的时候,methodizeClass方法会将 baseMethodList 添加到class_rw_t的methods列表中,之后会遍历category_list,并将category的方法也添加到methods列表中。
    这里的category指的是分类,基于此,category能扩充一个类的方法。这是开发时经常需要使用到。
    class_ro_t在内存中是不可变的。在运行期间,动态给类添加方法,实质上是更新class_rw_t的methods列表。
    baseProtocols与baseMethodList类似。
    objc_object、objc_class、class_rw_t、class_ro_t的关系如图3.4。

    图 3.4

    类的理解与方法的调用

    • 对象方法:前面提过,调用对象方法,相当于给对象发送消息,例如[obj methodWithArg: arg] 。 当obj_object接收到消息后,通过其isa指针找到对应的objc_class,objc_class又通过其data() 方法,查询class_rw_t的methods列表。若有,则返回;否则,到其父类寻找。以此类推,直到根类,若在根类中仍没有该方法,则crash。

    • 类方法: 在objc中,类本身也是一个对象。objc_class继承自objc_object,有一个isa指针,指向其所属的类,即meta class。可以这样理解,类是meta class 的对象。所以,当调用类方法是,例如[classObj methodWithArg: arg],classObj也会通过其isa指针到其所属的类(meta class)中寻找。这也就是为什么说,图1.1 里class 存储对象方法,meta class 存储类方法。

    • meta class的isa指针:meta class本身也是一个对象,它的isa指针指向的也是其所属的类。子meta class 的isa指针指向NSObjct 的meta class。 NSObjct 的meta class 的isa指针指向自身。当然,由于苹果进行了封装,在开发中基本不可能直接去使用meta class。

    对象的成员变量寻址
    前面提过,在objc_object中只有一个isa指针。实际上当我们调用 +alloc 方法来初始化一个对象时,也仅仅在内存中生成了一个objc_object结构体,并根据其instanceSize来分配空间,将其isa指针指向所属的类。
    类的成员变量ivar_t存储在class_ro_t中的ivar_list_t * ivars中,ivar_t的定义如下:

    struct ivar_t {
        int32_t *offset;
        const char *name;
        const char *type;
        uint32_t size;
    }
    

    其中offset 是成员变量相对于对象内存地址的偏移量,正是通过它来完成变量寻址。
    当我们使用对象的成员变量时,如 myObject.var ,编译器会将其转化为object_getInstanceVariable(myObject, 'var', **value) 找到其ivar_t结构体ivar,然后调用object_getIvar(myObject, ivar)来获取成员变量的内存地址。其计算公式如下:

    id *location = (id *)((char *)obj + ivar_offset);
    

    基于此,虽然多个对象的isa指针指向同一个objc_class,但由于对象的内存地址不一样,所以它们的实例变量存储位置也不一样,从而实现对象与类之间的多对一关系。

    相关文章

      网友评论

        本文标题:Objective-C Runtime机制简析

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