类的ISA及继承链
在对象的本质那篇文章里我们已经清楚了堆内存指针是通过isa和类进行关联起来的,也知道isa的64位域信息里存储的相关信息,我们可以在回顾下:
上图中,我们看到isa_t是一个联合体位域,我们看的位域存储的信息就从ISA_BITFIELD进入,看到如下信息:
上图是_x86_64架构下的位域存储信息,下图是_arm_64(iphone)架构下的位域信息。
那么我们类的相关信息就存储在
shiftcls中,之前我们已经知道可以通过内存平移拿到shiftcls存储的值,这里我们也可通过ISA_MASK(0x00007ffffffffff8ULL掩码)来获取到类的地址,通过 isa & ISA_MASK 可以取到类的地址,当然我们可以通过例子验证:
类的ISA指向——元类
我们前面已经知道是实例对象的isa指向类,那我们类的isa指向哪里呢?接下来我们可以通过代码分析:
上图中我们看到实例对象的isa指针指向类,类的isa对象指向元类,而元类的isa指针指向的地址和NSObject的isa指针指向的地址相同,都是0x00007fff86cbc638,而0x00007fff86cbc638的isa地址是它本身,也是0x00007fff86cbc638,所以我们可以确定它就是根元类。那么我们也就知道元类的isa指针指向的是根元类,同样NSObject的isa指针指向的也是根元类,根元类的isa指向自己。
那么元类之间有没有继承关系呢,我们看下面的示例:
上图中就可证明,元类的父类是父元类,若当前类直接继承于NSObject,那么它的元类的父类就是根元类。那NSObject继承谁呢?根源类又继承自哪里呢?
我们发现NSObject继承nil,根元类继承于NSObject。那么我们最后可以理解这张图:
类的内存结构
之前探究底层源码可以知道类的底层数据类型是objc_class,那么我们从源码中可以观察下它的数据结构:
从图中我们可以看到,objc_class这个结构体成员包括ISA(继承自objc_object),superclass(父类),cache(缓存),bits。我们平常声明的method和properties等信息基本都存在bits中,当然我们都可以通过lldb调试验证。
lldb调试验证类的bits存储信息
我们从上一个图objc_class中可以看到类的基本数据结构,那我们想要取到bits的值,只能通过内存平移来获取,首先知道ISA和superclass是结构体指针,那么大小都是8字节,关键就看cache的大小,接下来我们可以进去看看cache_t的数据结构:
这张图我们就很清晰了,第一个8字节是isa,第二个8字节就是superclass,第三个8字节就是cache_t。
下面这张图可以看到cache_t的数据结构:
其他都是方法或者静态变量,不在结构体内,而_originalPreoptCache和附近的struct处于联合体内(union),所以他们两个只需要取其中一个数据大小,最终cache_t的大小为16字节。那我们想获取bits的地址就是类的首地址偏移32个字节,如下图:
内存地址偏移32字节后得到bits地址后,还原为class_data_bits_t *类型。这里之所以强转类型,是为了后面方便取值。接下来我们获取下bits中的data数据,它是class_rw_t *类型的。
可以看到,我们取出的值里firstSubclass为nil,我们的CTPerson是有子类CTTeacher的,其实这里是系统做了优化,最关键的是我们CTPerson类里声明的属性和方法现在一个也没看到,他们在哪里呢?这时候我们就要去class_rw_t这个结构体里看具体的方法了:
很明显,我们可以看到想获取属性和方法,可以通过调用methods()和properties(),那我们lldb调试下:
上图中我们可以看到list_array_tt是个双层数组结构,里面是一个个list,我们接着调试:
我们可以到最终取出了属性名name,当然$8.get(1)是可以取出属性名hobby的,但是成员变量并没有在这里找到,下图可以看到里面只有两个属性,那声明的成员变量subject在哪里呢,我们再说。
既然我们能拿到相关的属性,那我们也可以拿到相关的方法,如下图:
这里方法取值跟属性还是有点区别的,结构体method_t里没有直接的成员变量来存储方法名、方法签名等相关信息,而是一个big结构体,所以取值需要获取big这个结构体。
我们从上图中可以看到声明类方法+(void)say666并不在其中,具体在哪里呢?我们下一篇文章讲解。