美文网首页
五、isa 和类结构分析

五、isa 和类结构分析

作者: 顺7zi燃 | 来源:发表于2020-09-20 21:17 被阅读0次

    主要内容:围绕展开探索
    一、isa 走位
     1.类的分析
      2.元类
      3.isa走位
      4.superClass 走位
    二、objc_class & objc_object
    三、类结构分析

    一、isa 走向

    主要分析:isa的走向 及的关系

    1.准备工作

    • 自定义两个类:LGPerson继承 NSObject, LGTeacher 继承 LGPerson
    @interface LGPerson : NSObject
    @end
    @interface LGTeacher : LGPerson
    @end
    
    • main函数中 实例化对象
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            LGPerson *person = [LGPerson alloc];
            LGTeacher *teacher = [LGTeacher alloc];
        }
        return 0;
    }
    

    2.元类

    main函数的 LGTeacher 部分加断点,lldb调试,调试过程:如图

    • image.png

    问题: 为什么在前面过程中,地址0x00000001000021c80x00000001000021a0 都得到 LGPerson, 到底哪个才是 LGPerson?

    • 0x00000001000021c8对象isa 指针地址 & isaMask 后得到对象的 isa 归属的类,也就是personisa的归属来自于 LGPerson.
    • 0x00000001000021a0类信息isa指针地址 & isaMask 后得到 类对象的归属,类对象的归属来自于元类
    • 所以,上面两个LGPerson,一个是类对象,一个是元类

    元类的定义

    • 什么是元类
      元类是系统给我们的非常重要的东西,对象isa归属 来自于也是一个对象 是类对象,那么类对象的归属是谁?苹果把类对象的归属给了元类
    • 为什么要有元类
      既然是一个对象,那就会有 方法、协议、属性等等一系列的存储和归属,那么方法等归属于谁?怎么去查看?不好查看,这时就需要在系统级别里面,需要元类这样的定义,所以就有元类元类定义创建都是由编译器自动完成

    结论的归属来自于元类,所有类方法的存储等等都存在元类里面

    3.isa走向图

    • 继续 lldb调试 ?
      image.png

    得到结论对象 的isa -> 的isa ->元类元类的isa -> NSObject
    引出问题元类isa 指向的NSObject不是内存中的 NSObject,那是什么?内存中会存在多个NSObject 类信息吗?

    验证内存中有几个类对象:

    void lgTestClassNum(){
        Class class1 = [LGPerson class];
        Class class2 = [LGPerson alloc].class;
        Class class3 = object_getClass([LGPerson alloc]);
        Class class4 = [LGPerson alloc].class;
        NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
    }
    

    输出结果:

    • image.png

    结论:

    • NSObject在内存中只存在一份,如果存在多份,每创建一个类的对象,就要多一份,但输出结果地址相同,说明在内存中永远只存在一份。
    • 元类isa 指向的NSObject是元类,NSObject 来自于根类,所以也叫根元类。

    验证:元类isa 指向的NSObject是元类

    • image.png

    得出以下结论:

    • NSObjet 的实例对象isa指向 NSObject 的类NSObject 的类isa指向 NSObject 的元类NSObject 的元类 就是 根元类根元类再指,指向的是 自己
    • 任何实例对象isa指向 , isa 指向元类元类isa 指向根元类
    • 特殊情况:比如说 子类父类Teacher 继承自 Person,他们也满足这样情况 Person的对象指向 Person类指向元类元类指向根元类。只有NSObject 免了一步 元类的指向,直接指向根元类

    如下图:

    • image.png

    代码验证:

    `
    void lgTestNSObject2(){
        // NSObject实例对象
        LGPerson *object1 = [LGPerson alloc];
        // NSObject类
        Class class = object_getClass(object1);
        // NSObject元类
        Class metaClass = object_getClass(class);
        // NSObject根元类
        Class rootMetaClass = object_getClass(metaClass);
        // NSObject根根元类
        Class rootRootMetaClass = object_getClass(rootMetaClass);
        NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
    }
    `
    

    输出结果:

    • image.png

    4.类的继承图

    思考问题LGTeacher 继承于LGPerson,实例化的对象 teacher 与 person 有关系吗? 答案是:没关系,因为实例对象没有继承关系,只有之间才存在继承关系,同样道理 元类也存在继承关系注意根元类 继承于 NSObject 也就是 根元类父类NSObject,万物皆是由NSObject 来创建的

    如图:

    • image.png

    之间 的继承关系

    • 实例对象之间没有 继承关系
    • 子类继承于父类父类继承于 NSObjectNSObject 继承于 nil,NSObject 是 万物之源
    • 子类的元类 继承于 父类的元类父类的元类 继承于根元类根元类 继承于 NSObject类(根元类父类NSObject)。

    二、objc_class & objc_object

    新问题:为什么 对象都有isa属性? 这里就要说两个结构体类型:objc_classobjc_object
    clang编译生成的 main.m文件中,可以看到如下c++源码

    • NSObject 在底层编译为 NSObject_IMPL 结构体

      • NSObject_IMPL结构体 中有 一个Class 类型的 isa 指针,其中Class是 objc_class 类型。
      • objc_class是一个结构体,在iOS中,所有的Class都是以objc_class 为模版创建的。
    typedef struct objc_class *Class;
    
    struct NSObject_IMPL {
        Class isa;
    };
    
    • 在 objc4 源码中搜索 objc_class的定义,共找到两个版本

      • 旧版 已废除 在 runtime.h

        已废除的objc_class.png
      • 新版在objc_runtime-new.h 中

      • 新版objc_class.png

      从新版的定义中,可以看到 objc_class 结构体类型是继承自 objc_object

    • 在 源码中搜索 objc_object {,可以找到 objc_object的定义

      objc_object.png

    问题:objc_class 与 objc_object 有什么关系?

    • 结构体类型objc_class 继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性

    • mian.cpp底层编译文件中,NSObject中的isa在底层是由Class 定义的,其中class的底层编码来自 objc_class类型,所以NSObject也拥有了isa属性

    • NSObject 是一个类,用它初始化一个实例对象objc,objc 满足objc_object 的特性(即有isa属性),主要是因为isa 是由 NSObjectobjc_class继承过来的,而objc_class继承自objc_objectobjc_objectisa属性。所以对象都有一个isaisa表示指向,来自于当前的objc_object

    • objc_object(结构体) 是 当前的 根对象所有的对象元类 都有这样一个特性 objc_object,即拥有isa属性

    【百度面试题】objc_object 与 实例对象的关系

    • 所有的对象都是以 objc_object 为模板继承来的。
    • NSObject 来自于OC 端。 objc_object来自于底层(c/c++
    • 总结: objc_object对象关系继承关系

    总结

    • 所有的对象 + + 元类 都有isa属性

    • 所有的对象都是由objc_object继承来的

    • 简单概括就是万物皆对象,万物皆来源于objc_object,有以下两点结论:

      • 所有以 objc_object为模板 创建的对象,都有isa属性

      • 所有以objc_class为模板,创建的,都有isa属性

    • 在结构层面可以通俗的理解为上层OC底层对接

      • 下层是通过 结构体 定义的模板,例如objc_class、objc_object
      • 上层 是通过底层模板创建的 一些类型,例如LGTeacher

    objc_class、objc_object、object、NSObject等的整体的关系,如图

    • image.png

    三、类结构分析

    主要是分析类的属性、方法、成员变量、协议...
    上面我们知道了类的结构是什么样的,那么类里面具体都包含了一些什么内容呢,下面我们就来分析一下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();
        }
        .......
    }
    
    • 第一个属性Class ISA 被注释掉的,意思就是从父类继承过来的,我们进入 objc_object里面可以看到只有一个isa,占用8个字节。
    • 第二个属性Class superclass父类,一个指针占用8个字节。
    • 第三个属性cache_t cache一个结构体,顾名思义是一些缓存的信息,总共占用16个字节
    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets; //  bucket_t 结构体指针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;//  typedef unsigned short 2字节
    #endif
        uint16_t _occupied;// typedef unsigned short  2字节
    }
    
    其他的static变量和方法均不存在结构体内存中,因此不占内存
    要获取bits ,就是首地址 平移32,也就是 + 0x20.
    
    • 第四个属性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();
     }
    
    struct class_data_bits_t {
        uintptr_t bits;
        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_t {
        ......
        const method_array_t methods() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>()->methods;
            } else {
                return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
            }
        }
    
        const property_array_t properties() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>()->properties;
            } else {
                return property_array_t{v.get<const class_ro_t *>()->baseProperties};
            }
        }
        const protocol_array_t protocols() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>()->protocols;
            } else {
                return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
            }
        }
    }
    

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

    @interface LGPerson : NSObject
    {
        NSString *hobby;
    }
    @property (nonatomic,copy) NSString *nickName;
    - (void) ins_sayHello;
    + (void) cls_sayHappy;
    
    @end
    

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

    • image.png

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

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

    • 通过p/x LGPerson.class直接获取首地址

    • 通过x/4gx LGPerson.class,打印内存信息获取

    • image.png

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

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

    • image.png
    • image.png

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

    通过以上打印可以看到,在class_rw_t中找到了我们所定义的nickName属性、对象方法ins_sayHello、nickName的setter/getter方法,但是成员变量hobby类方法cls_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;       //成员变量列表
        property_list_t *baseProperties; //属性列表
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
        ......
    }
    

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

    • image.png

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

    此时我们来总结一下:
    1.在class_rw_t里面存放的有methods、properties、protocols
    2.在class_ro_t里有baseMethodList、baseProperties、baseProtocols、ivars
    3.class_ro_t这个结构体是通过const定义,说明在编译时候就确定好了,后面取出来使用是不可以更改的。
    4.成员变量不生成setter/getter方法,并且存在class_ro_t的ivars里面。
    5.此时还有一个类方法cls_sayHappy没有找到。

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

    类的信息是如何存储的

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

    image.png

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

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

    探索类方法存储位置
    此时此刻我们就把class_rw_tclass_ro_t 存储类信息的过程探索的差不多了。
    但是类方法cls_sayHappy还没找到,既然我们在类里面没有找到cls_sayHappy,那么我们想一下它会存到哪里呢?通过上面分析的isa走位,猜想它会不存到元类里面去了,那就去元类找找看

    • image.png

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

    相关文章

      网友评论

          本文标题:五、isa 和类结构分析

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