美文网首页V10(逻辑教育)
iOS-底层探索05:类的结构分析

iOS-底层探索05:类的结构分析

作者: differ_iOSER | 来源:发表于2020-09-14 15:13 被阅读0次

    iOS 底层探索 文章汇总

    目录

    一、前言

    上一篇文章iOS 对象的本质我们分析了对象的底层结构,并在iOS isa底层结构分析中提到了 对象,类,元类,根元类等概念,这篇文章我们就一起来分析类的底层结构到底是什么。

    在开始探究之前,先补充一下 内存偏移 的概念,主要是为了更好理解后面的类的结构体。

    int c[4] = {1,2,3};             // 这里先定义一个int数组 c
    int *d   = c;                   // 然后定义一个指针d指向 c
    NANSLog(@"%p - %p - %p - %p",&c,&c[0],&c[1],&c[2]);
    NANSLog(@"%p - %p - %p",d,d+1,d+2);
    打印结果:0x7ffeefbff500 - 0x7ffeefbff500 - 0x7ffeefbff504 - 0x7ffeefbff508
    打印结果:                 0x7ffeefbff500 - 0x7ffeefbff504 - 0x7ffeefbff508
    看这里我们会发现 数组c 的地址 和 c[0] 是同一个地址, 而指针d也是等于 数组c的首地址
    并且通过指针d+1,d+2 也能找到数组相应的元素,所以说通过指针偏移可以指向接下来连续的内存地址。 
    

    二、类的结构分析

    1. 类的底层实现

    我们都知道,所有的类都是继承于NSObject,那NSObject本身不就是一个类吗?下面先结合源码来看一下

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            NSObject *obj = [[NSObject alloc] init];
        }
        return 0;
    }
    

    通过NSObject点击进入底层

    @interface NSObject <NSObject> {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
        Class isa  OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
    }
    

    这里我们可以看到 NSObject里面仅有一个Class isa,那么这个Class又是什么,继续点进去

    typedef struct objc_class *Class;
    typedef struct objc_object *id;
    

    这里我们可以看到Class是一个结构体,也就是之前说到的,类的本质就是一个结构体。而objc_class 又是继承自objc_object, 这也说明了我们常说的万物皆对象

    • NSObject本身是一个类,在底层实现就是objc_class
    • objc_objectc的结构类型,NSObjectOC的类型,NSObject就是对objc_object的封装。

    objc_objectobjc_class关系图如下:

    objc_object和objc_class关系图

    2. 类的属性、方法、成员变量、协议...分析

    上面我们知道了类的结构是什么样的,那么类里面具体都包含了一些什么内容呢,下面我们就来分析一下objc_class

    struct objc_class : objc_object {
        // Class ISA;              // 8字节
        Class superclass;          // 8字节
        cache_t cache;             // formerly cache pointer and vtable   16字节
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() const {
            return bits.data();
        }
        void setData(class_rw_t *newData) {
            bits.setData(newData);
        }
    ....省略后面的代码.....
    }
    

    1.第一个属性Class ISA 被注释掉的,意思就是从父类继承过来的,我们进入 objc_object里面可以看到只有一个isa,占用8个字节。

    struct objc_object {
    private:
        isa_t isa;
    
    public:
        // ISA() assumes this is NOT a tagged pointer object
        Class ISA();
    
    ....省略后面的代码.....
    };
    

    2.第二个属性Class superclass父类,一个指针占用8个字节。
    3.第三个属性cache_t cache一个结构体,顾名思义是一些缓存的信息,总共占用16个字节

    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets; // 结构体指针8个字节
        explicit_atomic<mask_t> _mask; //typedef uint32_t mask_t; int占4字节
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
        
       ....省略.....
    
    #if __LP64__
        uint16_t _flags;// 2字节
    #endif
        uint16_t _occupied;// 2字节
    }
    
    其他的static变量和方法均不存在结构体内存中,因此不占内存
    
    ....省略后面的代码.....
    

    4.第四个属性bits是什么?这里我们来看一下。

    typedef unsigned long           uintptr_t;
    struct class_data_bits_t {
        // 相当于 unsigned long bits; 占64位
        // bits实际上是一个地址(是一个对象的指针,可以指向class_ro_t,也可以指向class_rw_t)
        uintptr_t bits;
    ... 省略...
    }
    

    从这里可以看到bits应该就是一个64位的数据段,那么里面存了什么数据呢,还要继续往下分析。

    class_data_bits_t bits的注释:class_rw_t * plus custom rr/alloc flags,意思是class_data_bits_t就相当于class_rw_t * 加上rr/alloc标志。它提供了data()方法返回class_rw_t *指针。
    而在bits后面就紧接着声明了一个 class_rw_t * 指针,通过bits.data()返回,接下来就来看看这个bits.data()

     class_rw_t *data() {
         // 这里的bits就是上面定义的class_data_bits_t bits;
         return bits.data();
     }
    
    class_rw_t* data() const {
         // FAST_DATA_MASK的值是0x00007ffffffffff8UL
         //(lldb) p/t 0x00007ffffffffff8    打印二进制 看一下
         //(long) $0 = 0b0000000000000000011111111111111111111111111111111111111111111000
         // bits和FAST_DATA_MASK按位与,实际上就是取了bits中的[3,46]共44位
         return (class_rw_t *)(bits & FAST_DATA_MASK);
     }
    

    那么这个class_rw_t *是什么呢?

    struct class_rw_ext_t {
        const class_ro_t *ro;
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
        char *demangledName;
        uint32_t version;
    };
    
    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint16_t witness;
    #if SUPPORT_INDEXED_ISA
        uint16_t index;
    #endif
    
        explicit_atomic<uintptr_t> ro_or_rw_ext; 
    
        Class firstSubclass;
        Class nextSiblingClass;
    
    private:
        ...省略代码
    public:
         ...省略代码
        const class_ro_t *ro()
        void set_ro(const class_ro_t *ro)
        const method_array_t methods() 
        const property_array_t properties()
        const protocol_array_t protocols()
    

    在这个 class_rw_t结构体中我们发现这里有methods(方法)properties(属性)protocols(协议)这些信息,那么我们所需要的类中的方法、属性、成员变量等信息是不是在这里存储的呢?下面我们就用代码来验证下。

    @interface NAPerson ()
    {
        NSString *hobby;
    }
    
    @property (nonatomic, copy) NSString *nickName;
    - (void)sayHello;
    + (void)sayHappy;
    
    @end
    

    先定义一个NAPerson类,里面有 属性:nickName、 成员变量:hobby、 对象方法:sayHello、 类方法:sayHappy
    然后我们通过lldb指令打印查看,结合上面的分析,来看看这几个成员都存储在了什么地方

    类的地址

    在找bits的时候是通过内存偏移方法来找到,这也就是开头先补充的内存偏移的概念。 因为在objc_class的结构中,isa占8字节superclass占用8字节cache占用16个字节,将cls的地址偏移32个字节0x20便是bits的地址。

    获取类的首地址有两种方式

    • 通过p/x CJLPerson.class直接获取首地址
    • 通过x/4gx CJLPerson.class,打印内存信息获取
      两种获取类的内存地址方式

    注意:这里获取bits是通过类的内存地址加上偏移量而不是通过isa的地址加上偏移量,这也是类和数组不同的地方。类的地址也是第一个元素地址,只是通过x/4gx 读出来的是类地址后面存的值,不是第一个元素地址。isa的地址比类的地址低(0x28)40字节(当我们实现了类之后就会直接去处理元类,所以类和元类是连续的,而类的大小 到bit 之后正好40在,这里涉及到内存平移到元类)。

    通过查看class_rw_t定义的源码发现,结构体中有提供相应的方法去获取 属性列表、方法列表等

    属性列表 方法列表

    方法的符号绑定:对于v16@0:8
    v表示方法的返回值为void
    16表示方法参数的大小(通过clang编译的cpp文件可知该方法存在两默认的参数:(NAPerson * self, SEL _cmd))
    @表示方法的第一个参数类型
    0表示方法的第一个参数占用内存的起始位置
    :表示方法的第二个参数类型
    8表示方法的第二个参数占用内存的起始位置
    对于set方法会多一个参数所以方法的符号会有区别
    关于参数类型编码参考这个链接

    方法列表2

    通过以上打印可以看到,在class_rw_t中找到了我们所定义的nickName属性、对象方法sayHellonickNamesetter/getter方法,但是成员变量hobby类方法sayHappy都没有找到。
    此时再从class_rw_t找一找其他线索,发现有一个const class_ro_t *ro()的方法,该方法返回一个常量结构体指针,那么我们要找的成员变量和类方法会不会在这里呢,点进去看一下

    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
    #ifdef __LP64__
        uint32_t reserved;
    #endif
    
        const uint8_t * ivarLayout;
        
        const char * name;
        method_list_t * baseMethodList;  //方法列表
        protocol_list_t * baseProtocols; //协议列表
        const ivar_list_t * ivars;       //成员变量列表
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties; //属性列表
    
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
    ....省略代码
    }
    

    进来一看发现,这里竟然跟class_rw_t看起来差不多,同样有方法、属性、协议列表,而且还有一个ivars 列表,那么这个ivars会不会就是成员变量列表呢。接下来继续用lldb指令来查看

    打印ro

    果然,我们定义的成员变量hobby是在class_ro_t里面的。同样在baseMethodListbaseProperties里面也找到了我们所定义的属性和对象方法,这里就不截图了。

    此时我们来总结一下:

    1.在class_rw_t里面存放的有methodspropertiesprotocols
    2.在class_ro_t里有baseMethodListbasePropertiesbaseProtocolsivars
    3.class_ro_t这个结构体是通过const定义,说明在编译时候就确定好了,后面取出来使用是不可以更改的。
    4.成员变量不生成setter/getter方法,并且存在class_ro_tivars里面。
    5.此时还有一个类方法sayHappy没有找到。

    通过以上分析我们大概可以知道,类的属性成员变量方法协议等信息存在什么位置了。但是class_rw_tclass_ro_t为什么会存了一些相同的信息呢?这就需要我们进一步的分析了。

    3. 内的信息是如何存储的

    通过前面的分析,在class_rw_t结构中可以拿到类的属性等相关信息了,class_ro_t结构中可以拿到类的成员变量等信息,由此就形成以这样的一个结构。

    类中数据的存储结构

    通过上图可以得出以下一些结论:

    • 通过@property定义的属性,会存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含属性
    • 通过{}定义的成员变量,也会存储在类的bits属性中,通过bits --> data() -->ro() --> ivars获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量

    4. 探索类方法存储位置

    此时此刻我们就把class_rw_tclass_ro_t 存储类信息的过程探索的差不多了。
    但是类方法sayHappy还没找到,既然我们在类里面没有找到sayHappy,那么我们想一下它会存到哪里呢?通过iOS isa底层结构分析这篇文章分析,猜想它会不存到元类里面去了,那就去元类找找看

    元类探索1 元类探索2

    从上图打印来看,我们在元类里面找到了类方法sayHappy,证明了类方法是存在元类里面的。此刻我们所生命的方法、属性、成员变量已经全部找到了,也大概了解了类的结构以及类的成员信息都存在哪里。

    三、【百度面试题】objc_object 与 对象的关系

    • 所有的对象 都是以 objc_object为模板继承过来的
    • 所有的对象 是 来自 NSObject(OC) ,但是真正到底层的 是一个objc_object(C/C++)的结构体类型

    【总结】objc_object 与 对象的关系 是 继承关系

    总结

    • 所有的对象 + + 元类 都有isa属性
    • 所有的对象都是由objc_object继承来的
    • 类的本质是一个struct objec_class:objc_object结构体, 万物皆对象,类也- 是一个对象。
    • 属性会自动生成setter/getter方法,成员变量不会。并且属性在编译之后会生成带有_的成员变量存储在ivars里面。
    • 类的对象方法存在本类当中,而类方法存在元类中

    相关文章

      网友评论

        本文标题:iOS-底层探索05:类的结构分析

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