OC类原理分析——(上)

257 阅读4分钟

类的ISA及继承链

对象的本质那篇文章里我们已经清楚了堆内存指针是通过isa和类进行关联起来的,也知道isa的64位域信息里存储的相关信息,我们可以在回顾下:

截屏2022-04-18 下午2.43.07.png

截屏2022-04-18 下午2.59.04.png

上图中,我们看到isa_t是一个联合体位域,我们看的位域存储的信息就从ISA_BITFIELD进入,看到如下信息:

截屏2022-04-18 下午2.53.07.png

上图是_x86_64架构下的位域存储信息,下图是_arm_64(iphone)架构下的位域信息。

截屏2022-04-18 下午2.53.19.png 那么我们类的相关信息就存储在shiftcls中,之前我们已经知道可以通过内存平移拿到shiftcls存储的值,这里我们也可通过ISA_MASK(0x00007ffffffffff8ULL掩码)来获取到类的地址,通过 isa & ISA_MASK 可以取到类的地址,当然我们可以通过例子验证:

截屏2022-04-18 下午3.22.29.png

类的ISA指向——元类

我们前面已经知道是实例对象的isa指向类,那我们类的isa指向哪里呢?接下来我们可以通过代码分析:

截屏2022-04-18 下午3.42.51.png

上图中我们看到实例对象的isa指针指向类,类的isa对象指向元类,而元类的isa指针指向的地址和NSObjectisa指针指向的地址相同,都是0x00007fff86cbc638,而0x00007fff86cbc638isa地址是它本身,也是0x00007fff86cbc638,所以我们可以确定它就是根元类。那么我们也就知道元类的isa指针指向的是根元类,同样NSObjectisa指针指向的也是根元类,根元类的isa指向自己。

那么元类之间有没有继承关系呢,我们看下面的示例:

截屏2022-04-18 下午4.41.20.png

上图中就可证明,元类的父类是父元类,若当前类直接继承于NSObject,那么它的元类的父类就是根元类。那NSObject继承谁呢?根源类又继承自哪里呢?

截屏2022-04-18 下午4.53.06.png

我们发现NSObject继承nil,根元类继承于NSObject。那么我们最后可以理解这张图:

截屏2022-04-19 下午1.48.44.png

类的内存结构

之前探究底层源码可以知道类的底层数据类型是objc_class,那么我们从源码中可以观察下它的数据结构:

截屏2022-04-18 下午5.09.11.png

从图中我们可以看到,objc_class这个结构体成员包括ISA(继承自objc_object),superclass(父类),cache(缓存),bits。我们平常声明的methodproperties等信息基本都存在bits中,当然我们都可以通过lldb调试验证。

lldb调试验证类的bits存储信息

我们从上一个图objc_class中可以看到类的基本数据结构,那我们想要取到bits的值,只能通过内存平移来获取,首先知道ISAsuperclass是结构体指针,那么大小都是8字节,关键就看cache的大小,接下来我们可以进去看看cache_t的数据结构:

截屏2022-04-18 下午8.06.45.png

这张图我们就很清晰了,第一个8字节是isa,第二个8字节就是superclass,第三个8字节就是cache_t。

下面这张图可以看到cache_t的数据结构:

截屏2022-04-18 下午8.31.39.png

其他都是方法或者静态变量,不在结构体内,而_originalPreoptCache和附近的struct处于联合体内(union),所以他们两个只需要取其中一个数据大小,最终cache_t的大小为16字节。那我们想获取bits的地址就是类的首地址偏移32个字节,如下图:

截屏2022-04-18 下午8.55.41.png

内存地址偏移32字节后得到bits地址后,还原为class_data_bits_t *类型。这里之所以强转类型,是为了后面方便取值。接下来我们获取下bits中的data数据,它是class_rw_t *类型的。

截屏2022-04-19 下午12.07.38.png

截屏2022-04-19 上午11.58.02.png

可以看到,我们取出的值里firstSubclassnil,我们的CTPerson是有子类CTTeacher的,其实这里是系统做了优化,最关键的是我们CTPerson类里声明的属性和方法现在一个也没看到,他们在哪里呢?这时候我们就要去class_rw_t这个结构体里看具体的方法了:

截屏2022-04-19 下午12.12.35.png

很明显,我们可以看到想获取属性和方法,可以通过调用methods()properties(),那我们lldb调试下:

截屏2022-04-19 下午12.22.58.png

截屏2022-04-19 下午12.23.40.png

截屏2022-04-19 下午12.26.09.png

上图中我们可以看到list_array_tt是个双层数组结构,里面是一个个list,我们接着调试:

截屏2022-04-19 下午12.58.05.png

我们可以到最终取出了属性名name,当然$8.get(1)是可以取出属性名hobby的,但是成员变量并没有在这里找到,下图可以看到里面只有两个属性,那声明的成员变量subject在哪里呢,我们再说。

截屏2022-04-19 下午1.04.40.png

既然我们能拿到相关的属性,那我们也可以拿到相关的方法,如下图:

截屏2022-04-19 下午1.26.34.png

截屏2022-04-19 下午1.35.24.png

这里方法取值跟属性还是有点区别的,结构体method_t里没有直接的成员变量来存储方法名、方法签名等相关信息,而是一个big结构体,所以取值需要获取big这个结构体。

截屏2022-04-19 下午1.36.50.png 截屏2022-04-19 下午1.37.05.png

我们从上图中可以看到声明类方法+(void)say666并不在其中,具体在哪里呢?我们下一篇文章讲解。