美文网首页
Runtime 一:类与对象基础结构

Runtime 一:类与对象基础结构

作者: iOS_修心 | 来源:发表于2021-09-26 19:26 被阅读0次

    Demo: https://github.com/iOSlixiang/RuntimeTest.git
    objc4-818.2 源码: https://github.com/iOSlixiang/objc4-818.2.git

    Class

    Objective-C类是由Class类型来表示的,它实际上只是objc_class的结构体指针。

    typedef struct objc_class *Class;
    typedef struct objc_object *id;
    
    @interface Object { 
        Class isa; 
    }
    
    @interface NSObject <NSObject> {
        Class isa  OBJC_ISA_AVAILABILITY;
    }
    
    

    在2006年苹果发布Objc 2.0,objc_class定义没有发生变化,但是结构体发生了变化。
    在Objc2.0之前,objc_class源码

    // 一个类的实例的结构体
    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY;
    };
    // 类结构体
    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
        Class super_class                       OBJC2_UNAVAILABLE;  // 父类
        const char *name                        OBJC2_UNAVAILABLE;  // 类名
        long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
        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;
    

    Objc2.0,objc_class继承于objc_object。在objc_class中也会包含isa_t类型的结构体isa。 Objective-C 中类也是一个对象。

    struct objc_object {
    private:
        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
    }
    // isa是一个联合体
    union isa_t 
    {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
        Class cls;
        uintptr_t bits;
    }
    

    把源码的定义转化成类图,就是上图的样子。

    从上述源码中,我们可以看到,Objective-C 对象都是 C 语言结构体实现的,在objc2.0中,所有的对象都会包含一个isa_t类型的结构体。

    objc_object被源码typedef成了id类型,这也就是我们平时遇到的id类型。这个结构体中就只包含了一个isa_t类型的结构体。

    bjc_class继承于objc_object。所以在objc_class中也会包含isa_t类型的结构体isa。至此,可以得出结论:Objective-C 中类也是一个对象。在objc_class中,除了isa之外,还有3个成员变量,一个是父类的指针,一个是方法缓存,最后一个这个类的实例方法链表。

    object类和NSObject类里面分别都包含一个objc_class类型的isa。

    上图的左半边类的关系,右边是isa的结构体

    isa指针

    1. 联合体:一种特殊的数据类型,其目的是节省内存。联合体内部可以定义多种数据类型,但是同一时间只能表示某一种数据类型,且所有的数据类型共享同一段内存。联合体的内存大小等于所定义的数据类型中占用内存的最大者。
    2. 互斥赋值/共用内存:允许装入该“联合”所定义的任何一种数据成员,但同一时间只能表示一种数据成员,采用了覆盖的技术;
    3. union所占内存长度:union 变量所占用的内存长度等于最长的成员的内存长度;
    union isa_t { //联合体
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
        //提供了cls 和 bits ,两者是互斥关系
        Class cls;
        uintptr_t bits;
    #if defined(ISA_BITFIELD)
        struct {
            ISA_BITFIELD;  // defined in isa.h
        };
    #endif
    };
    

    isa_t类型使用联合体的原因也是基于内存优化的考虑,这里的内存优化是指在isa指针中通过char + 位域(即二进制中每一位均可表示不同的信息)的原理实现。通常来说,isa指针占用的内存大小是8字节,即64位,已经足够存储很多的信息了,这样可以极大的节省内存,以提高性能

    isa_t的定义中可以看出:

    • 提供了两个成员,clsbits,由联合体的定义所知,这两个成员是互斥的,也就意味着,当初始化isa指针时,有两种初始化方式

      • 通过cls初始化,bits无默认值

      • 通过bits初始化,cls无默认值

    • 还提供了一个结构体定义的·位域·,用于存储类信息及其他信息,结构体的成员·ISA_BITFIELD·,这是一个宏定义,有两个版本 __arm64__(对应ios 移动端) 和 __x86_64__(对应macOS)

    位域的宏定义

    元类(Meta Class)

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

    NSArray *array = [NSArray array];
    

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

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

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

    meta-class之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的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类的一个继承体系了,如下图所示:


    图中实线是 super_class指针,虚线是isa指针。

    • Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
    • 每个Class都有一个isa指针指向唯一的Meta class
    • Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
    • 每个Meta class的isa指针都指向Root class (meta)。

    对于NSObject继承体系来说,其实例方法对体系中的所有实例、类和meta-class都是有效的;而类方法对于体系内的所有类和meta-class都是有效的。

    cache_t

    struct cache_t {
        struct bucket_t *_buckets;
        mask_t _mask;
        mask_t _occupied;
    }
    
    typedef unsigned int uint32_t;
    typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
    
    typedef unsigned long  uintptr_t;
    typedef uintptr_t cache_key_t;
    
    struct bucket_t {
    private:
        cache_key_t _key;
        IMP _imp;
    }
    

    根据源码,我们可以知道cache_t中存储了一个bucket_t的结构体,和两个unsigned int的变量。

    mask:分配用来缓存bucket的总数。
    occupied:表明目前实际占用的缓存bucket的个数。

    bucket_t的结构体中存储了一个unsigned long和一个IMP。IMP是一个函数指针,指向了一个方法的具体实现。

    cache_t中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。

    Cache的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

    class_data_bits_t

    // class_data_bits_t相当于 class_rw_t指针加上 rr/alloc 的标志。
    struct class_data_bits_t {
        // Values are the FAST_ flags above.
        uintptr_t bits;
    }
    // 返回 class_rw_t *指针的快捷方法
    class_rw_t *data() {
        return bits.data();
    }
    
    
    // class_ro_t 在编译期产生,它是类中的可读信息。
    // class_rw_t 在运行时产生,它是类中的可读写信息。
    struct class_rw_t {
        uint32_t flags;    // 标记
        uint32_t version;  // 版本
    
        // 类中的只读信息
        const class_ro_t *ro;
    
        /*
         这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
         methods中,存储 method_list_t ----> method_t
         二维数组,method_list_t --> method_t
         这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
         */
        method_array_t methods;      // 方法数组
        property_array_t properties; // 属性数组
        protocol_array_t protocols;  // 协议数组
    
        Class firstSubclass; //为某个类第一次创建的时候去寻找父类的时候绑定给父类的。
        Class nextSiblingClass; // 下一个相同父类的类
    
        char *demangledName;
    }
    
    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart; // 开始位置
        uint32_t instanceSize; // 所占空间大小
    #ifdef __LP64__
        uint32_t reserved;
    #endif
    
         /* ivar 布局, 在编译期这里就固定了,
         它标示ivars的内存布局,在运行时不能改变,
         这也是为什么我们在运行时不能动态给类添加成员变量的原因*/
        const uint8_t * ivarLayout;
        
        const char * name;  //名字
        method_list_t * baseMethodList;  //方法链表
        protocol_list_t * baseProtocols; //协议链表
        const ivar_list_t * ivars;       //成员变量链表
    
        const uint8_t * weakIvarLayout; // weak 成员变量的内存布局
        property_list_t *baseProperties;  //属性链表
    
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
    };
    
    

    Objc的类的属性、方法、以及遵循的协议在obj 2.0的版本之后都放在class_rw_t中。class_ro_t是一个指向常量的指针,存储来编译器决定了的属性、方法和遵守协议。rw-readwrite,ro-readonly
    在编译期类的结构中的 class_data_bits_t *data指向的是一个 class_ro_t *指针。
    在运行时调用 realizeClass方法,会做以下3件事情:

    1. 从 class_data_bits_t调用 data方法,将结果从 class_rw_t强制转换为 class_ro_t指针
    2. 初始化一个 class_rw_t结构体
    3. 设置结构体 ro的值以及 flag

    最后调用methodizeClass方法,把类里面的属性,协议,方法都加载进来。

    整个流程 objc_readClassPair --> readClass --> realizeClass --> methodizeClass

    总结一下objc_class 1.0和2.0的差别。

    相关文章

      网友评论

          本文标题:Runtime 一:类与对象基础结构

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