OC底层学习-类的底层结构探究上篇

602 阅读11分钟

不管你学习的有多慢,只要不原地踏步那也是一种进步!

前言

上一章节中探索了对象的本质和isa关联类的过程,了解到对象的本质是一个结构体,而且是通过其内部成员变量isa关联到的,那又是个什么东西呢?那就进入本章节的探讨内容。

类(Class)

OC中万物皆对象,而对象中都会存在一个isa指向类,那么类的isa又是指向哪呢,有什么作用呢,下面通过代码和lldb调试来看一看:

isa指向

  • 对象isa指向

创建一个LhkhPerson类,实例化一个对象p

image.png

这边验证了一下对象的isa指向类 对象isa ---> ,接着得到类的指针地址0x0000000100794520,得到类的地址,那么是不是也可以看一下类的结构呢?接着lldb调试:

image.png

  • isa指向

通过lldb继续调试上面获取到的类的地址,得到类的isa,然后也通过与上掩码0x00007ffffffffff8,得到一个新的地址**0x00000001007944f8**,输出得到也是LhkhPerson,所以得出类的isa--->与类同名的一个类

  • VS同名类

这就让我们百思不得其解了,都是LhkhPerson,为什么指针地址不一样呢?到底哪个才是LhkhPerson真正的指针地址呢?接下来通过代码验证一下:

image.png

通过上面这段代码打印发现都是0x0000000100794520,那可以确定LhkhPerson的指针地址就是这个,那0x00000001007944f8这个地址到底是什么呢?

  • 元类诞生

这个时候需要通过MachOView工具来分析一下machO可执行文件

image.png 然后右击 show in Finder ,再右击显示包内容,将可执行文件拖入到MachOView工具上,

image.png

通过MachOView分析可以看到在项目编译过后,通过查看符号表,发现系统给LhkhPerson生成了一个元类,而上面遗留的那个问题对应的类正是这个元类

  • 元类总结

    • 通过上面分析,元类是由系统编译过后产生的,的名称和其关联的元类的名称是一样的;
    • 对象的isa指向isa指向元类
  • 元类isa指向

元类有没有类结构呢,或者其isa又是指向谁呢?接着这个问题我们往下走:

image.png 继续通过lldb发现元类isa指向根元类,而根元类isa指向根元类自己。

  • isa指向总结

通过上面代码调试和MachOView工具的分析最终得到了isa的指向:

对象---isa--->---isa--->元类---isa--->根元类---isa--->根元类

根据这个可以得到一个isa指向图

未命名文件(9).png

isa的指向图探索出来了,我们一般在项目中都会有类继承,那么class是怎么继承1的呢?

class类的继承链

- 类继承链

image.png LhkhTeacher是继承自LhkhPersonLhkhPerson是继承自NSObject,通过打印LhkhTeatherLhkhPerson还有NSObject的父类,得出它们之间的继承链为LhkhTeather---继承--->LhkhPerson---继承--->NSObject 既然类都有一条继承链,那么元类呢?

- 元类继承链

image.png

LhkhPerson继承自NSObject,打印可以得到其元类,根元类,根根元类地址都是0x7fff80030638,而LhkhPerson元类的父类打印地址结果也为0x7fff80030638,并且得到这个地址指向NSObject,即根元类;那么是不是可以得到任何一个对象的元类的父类就是根元类呢?

image.png LhkhTeacher是继承自LhkhPersonLhkhPerson继承自NSObject,那如果根据上面的结论就是LhkhTeacher的元类的父类也是根元类,但是打印结果告诉我们,并非如此,子类的元类的父类是就是子类的父类的元类,为什么会这样呢?因为元类也是有一条继承链,LhkhTeacher元类---继承--->LhkhPerson元类---继承--->根元类NSObject。 到这相信大家就会出现一个疑问,类继承链会到达根类NSObject,元类的继承会到达根元类NSObject,那么根类NSObject和根元类NSObject继承自什么呢?

- 类和元类继承特殊情况

image.png

根据打印结果可以得到,根类的父类null,即根类没有父类,而根元类的父类根类

- 类继承链总结

  1. 子类---继承--->父类---继承--->根类--->nil
  2. 子类元类---继承--->父类元类---继承--->根元类---继承--->根类--->nil 根据这个可以得出类的继承图

未命名文件(10).png

- Apple官方 isa和类的走向图

isa流程图.png

类的底层结构

探究类的底层结构前我们需要先了解一下内存偏移

内存偏移补充

探讨这个我们通过普通指针,对象指针,数组指针三个例子来看:

1. 普通指针

image.png 定义了两个局部变量ab都等于10,通过打印可以看到它们的值都是10,而它们的地址是不同的,a的地址为0x7ffee9501c24b的地址为0x7ffee9501c20

则得出

  • 根据ab为局部变量,和其地址0x7开头我们可以知道其存储在栈区域
  • 栈区域地址是由高指向低地址,而ab相差4字节,因为aint型,需要占用4字节,所以ab的偏移量为4字节。
2.对象指针

image.png

定义了两个对象obj1obj2,通过打印的地址我们分析一下:

  • 可以看出obj1的地址为0x600002440100是通过alloc开辟出来的,而obj2的地址为0x600002440110也是通过alloc开辟出来的,通过0x6开头和他们是手动开辟的我们知道他们是存在堆区的;
  • 再看两者的取地址,也就是指向obj1obj2堆地址的地址,分别是0x7ffeec5bcc080x7ffeec5bcc00,可以看出是在栈区,且由于obj1位对象占用8字节,所以obj1obj2的偏移量为8字节。
3.数组指针

image.png 定义一个数组指针c,和一个指针d,通过打印可以得到数组指针c的地址为0x7ffeebfa1c20,而c的第一个元素指针地址也为0x7ffeebfa1c20,后面的元素地址也是每隔4个字节(因为数组指针里面存的是int型数据,int型占4个字节);而指针d初始赋值是c,所以打印d地址就是0x7ffeebfa1c20,而d偏移一个步长(这边的步长也是由c里面的元素类型决定,我们这边存的是int型,所以偏移4字节);通过上面你的分析我们可以得到:

  • 数组的首地址就是其第一个元素的地址,也就是内存地址的首地址就是第一个元素的地址;
  • 数组里面的下一元素的地址可以通过首地址加上上一个元素偏移量(取决于数组里面存的元素类型)得到,也就是内存偏移可以根据首地址偏移量方法获取相对应变量的地址。

类结构

在探索对象的本质的时候就已经发现了class的本质其实是一个 objc_Class *结构体指针,在objc源码中直接搜索objc_Class *可以得到; image.png 那么objc_Class又是个什么呢?再次搜索一下objc_Class 挥发想能找到两个定义

image.png 这一个注意在objc2已经不使用了,所以直接看下面这个

image.png 可以发现objc_Class是继承自objc_objectimage.png 通过分析源码,可以发现除了一些方法,其内部有4个成员变量,ISAsuperclasscachebits

  • ISA:这个是一个隐藏属性,是继承于objc_object里面的成员变量isa,是一个结构体指针,占用8个字节,主要是用于关联类的;
  • superclass:也是一个Class类型,那么本质也就是结构体指针,也就是当前类的父类,也是占用8个字节;
  • cache:是一个cache_t类型,本质是一个结构体,用于优化方法调用,这个后面章节会讲解;
  • bits:是一个class_data_bits_t类型,本质也是一个结构体,主要用来存储类的属性,方法等数据的,这也是我们需要探讨的重点。

那我们想要探讨bits里面的内容,必须要获取到其内存地址,根据上面的内存偏移,想要得到bits的地址,还差一个cache的内存大小,由于cache_t是结构体所以其内存大小取决于内部变量,所以我们暂时不知道大小那我们先来看看cache_t其内部结构:

cache内存大小

image.png 通过源码中command左击cache_t进入到cache_t这个结构体中,有很多的方法和static修饰的变量,由于方法和全局变量都不是在计算结构体内存之中,无关重要,所以这边我们只需要看下面这两个变量:

  • explicit_atomic<uintptr_t> _bucketsAndMaybeMask:是一个泛型的结构体,真正的大小由uintptr_t(无符号长整型,占用8字节)决定,所以可以得到其占用8字节;
  • 联合体union:里面只有一个变量一个结构体,
    • 这个结构体中有3个变量:可以得出结构体大小为8字节
      • _maybeMask:一个mask_t(uint32_t)型的结构体 占用4个字节
      • _flags: uint16_t类型 占用2字节
      • _occupied: uint16_t类型 占用2字节
    • _originalPreoptCache:是一个结构体指针类型,占用8字节 而联合体是里面是互斥的,那么可以得到联合体大小为8字节。
union {
    struct {
        explicit_atomic<mask_t>    _maybeMask; //4
#if __LP64__
            uint16_t               _flags; //2
#endif
            uint16_t               _occupied;//2
        };
    explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    }

所以通过上面分析可以得到cache_t占用8+8,也就是16个字节,那么我们就可以计算得到bits的地址了,也就是首地址+32个字节的偏移量(8+8+16)。

bits探究

通过查找发现 image.png

class_rw_t *data() const {
  return bits.data();
}

这个方法里面返回bits.data()也就是我们bits里面的数据,那我们需要查看bits里面的内容,就先来看看class_rw_t

- class_rw_t探究

通过源码,我们进入到class_rw_t里面,里面有很多的方法,我们发现几个我们熟悉的:

image.png

image.png 这里面存储这我们的方法,属性,协议等数据,那我们通过lldb来探索一下:

image.png

  1. 我们通过x/4gx p.class 获取到类的首地址为0x100008b20
  2. 然后通过上面探究出来的要想获取到bits的地址需要偏移32个字节,也就是0x20,并强转成class_data_bits_t*,得到地址为$2 = 0x0000000100008b40
  3. 通过 return bits.data(),我们也通过p $2->data()得到(class_rw_t *) $3 = 0x000000010126f810,到此我们已经得到了class_rw_t的指针地址;
  4. 通过指针地址取值*$3最终得到class_rw_t里面的数据。

那既然得到了class_rw_t数据结构了,我们又知道他内部存储了方法,属性,协议等,我们就来看看: 这个是LhkhPerson里面的一些属性和方法 image.png

  • 属性

image.png

  1. 通过上面得到的class_rw_t的地址我们取出值,然后获取其属性列表p $4.properties()
  2. 然后就一层一层往里面取p $5.listp $6.ptr,然后再通过地址取值p *$7这时候就真正得到了我们属性的列表;
  3. 通过p $8.get(0)获取第一个属性,得到我们的name,和我们定义的是一样的。
  • 成员变量补充 我们上面探究属性的时候没有添加成员变量,那么会不会成员变量也会存在属性表中呢? image.png

image.png 我们这边发现只能打印出我们的name属性,再取的时候就会越界错误,所以成员变量nickName没有存在属性表中,那么存在哪呢?

我们通过查找class_rw_t的方法实现时发现里面还存在一个方法class_ro_t,我们顺着进去查看实现,

image.png

通过查看class_ro_t方法实现我们发现其存在一个ivar_list_t *型变量ivars,这个词我们应该很熟悉啊,这不就是实例变量表吗,那我们大概知道了实例变量应该就存在这个表里面了,我们来验证一下看看

image.png

image.png

  1. 我们通过之前的步骤获取到class_rw_t的地址并取值,然后通过p $4.ro()获取到class_ro_t地址;
  2. 我们通过class_ro_t地址取到值,然后通过p $6.ivars获取到实例变量列表的地址,并通过p *$7取值;
  3. 通过p $8.get(0)取到里面的实例变量,成功打印了我们的nickName成员变量;
  4. 我们继续取值发现里面还存了_name,而我们知道这个name是我们定义的属性,所以我们还可以得到,系统给属性生成的_属性名的成员变量并存在了class_ro_tivar_t中。

总结

class_rw_t方法中会调用class_ro_t这个方法,并通过里面的ivarsivarsivar_list_t这个类型,这是一个泛型,实质是ivar_t)实例变量表ivar_t存储成员变量和系统生成的属性的_属性名的变量。

  • 方法 我们使用同样方式探索一下方法列表:

image.png image.png 我们这边获取到方法名的时候有个注意点,由于method_t系统做了处理,是一个结构体,而他把方法名等数据放到了他里面的一个结构体变量big下,所以这边我们取到方法名时后面需要带上一个.big().

  • 类方法补充 image.png

我们继续获取方法,发现属性的getter和setter方法过后,在获取就报错越界了,但是并没有获取到我们声明的+(void)sayHello类方法,那我们声明的那个类方法呢?

其实通过上面的对象方法探究我们知道对象方法存在当中,那么我们知道OC中万物皆对象,那我们猜想类方法会不会就存在元类当中呢?

image.png

我们通过p/x object_getClass(LhkhPerson.class)获取到LhkhPerson的元类,然后按照上面探讨对象的方法一样会中获取到类方法+(void)sayHello,所以我们可以得到:

对象方法存在类结构的bits里面,而类方法存在元类结构的bits里面。

总结

通过上面的探究我们可以知道类的底层结构主要是四个变量ISAsuperClasscache,bits,而我们的bits里面存储了属性对象方法协议等数据。