本文主要内容
1.isa指向关系
2.类与元类的继承关系
3.通过内存平移访问bits数据
4.获取bits里面的methodlists
5.获取bits里面的propertylist
一、isa指向关系
上一篇研究了对象的底层原理,了解到实例对象中存储了一个8字节的isa指针,这个isa指针指向了该对象所属的类。此篇文章开始研究类的底层原理。 要研究类,首先获取类对象的内存地址,以HGPerson类为例,有3种获取方式:[HGPerson class]、[HGPerson alloc].class 、object_getClass([HGPerson alloc])。如下图,3种方式获取的类对象内存地址完全相同,因此说明类对象存在且只有一个。
类又称类对象
类本质是一个叫作objc_class的结构体,objc_class继承自objc_object,而对象的本质就是叫作objc_object的结构体。类中的isa指针也是由objc_object结构体中继承来的。所以说类也是对象。下图即为类的底层函数:
复制代码
接下来研究类对象的isa的指向。先从HGPerson的实例对象p开始,实例对象p的isa指针指向类对象HGPerson(上一篇已证明),接着HGPerson类对象的isa指针与上ISA_MASK后得到的内存地址,用po查看发现也为HGPerson“类对象”,但是其内存地址和实际的HGPerson类的地址不同,此对象为HGPerson的元类对象,与HGPerson类对象名称相同,内存地址不同。接着,元类对象的isa指针指向根元类NSObject,此根元类对象NSObject与根类NSObject的内存地址不同。接着,根元类对象NSObject的isa指针指向的内存地址和根元类对象NSObject的内存地址相同,所以根元类对象NSObject的isa指针指向根元类NSObject本身。
注意⚠️:通过观察上图中底部的内存地址发现,根元类对象的isa指针与根元类对象内存地址相同,isa指针作用是用来存储内存地址,isa指针地址和根元类地址相同说明该isa指针是纯粹的用来存储内存地址的指针,不属于“nonpointerIsa”,该根元类isa没有存储其他数据内容。
复制代码
总结
如下为isa的指向关系图
二、类与元类的继承关系
研究类与元类的继承关系。创建父类HGPerson、继承于HGPerson的子类HGTeacher。如下图,打印NSObject实例对象、NSObject类对象、NSObject元类(根元类)、HGPerson的元类、HGPerson的元类的父类、HGTeacher的元类、HGTeacher的元类的父类、NSObject的父类、根元类的父类的内存地址,发现: HGPerson的元类的父类就是NSObject元类(根元类);HGTeacher的元类的父类就是HGPerson的元类,也即元类的父类就是父类的元类;NSObject的父类为null,即NSObject没有父类;根元类的父类就是NSObject类对象。
总结
如下为类与元类的继承关系图
疑问:在上述研究过程中,出现了一个叫作元类的东西,于是产生一个疑问:什么是元类?为什么要引出元类呢?【不知道大家有没有这个疑问】这个问题在后续文章中会进行详细解释!
三.通过内存平移访问bits数据
文章开头讲到类的本质是一个叫作objc_class的结构体。我们也从前篇了解到实例对象包含isa和成员变量的值,那么类对象存储了哪些内容呢(即结构体objc_class是怎样的)? 从objc4-838.1源码找到结构体objc_class。其中包含ISA、superclass、cache、bits等,要想搞清楚中存储了哪些数据,需要读取到它的内存空间。
知识小亮点
1.内存平移:如下图举例说明:其中数组指针d,既表示数组c的首地址,也是数组中第一个元素的地址,"d+1"即为数组中第二个元素地址,"d+1"被称作内存平移,其中"1"为数组中单个元素长度(此处为4字节),使用*(d+i)即可获取到(d+i)对应内存地址中的值。
2.结构体内存为连续的,并且结构体内数据的存储是按顺序的,所以可以通过内存平移得到bits的内存地址。
复制代码
找到bits的内存地址: 通过lldb命令x/6gx p1.class可获取类对象的内存地址,通过类对象的内存平移即可获得bits的内存地址:在类对象首地址的基础上平移32字节(ISA8字节+父类superclass8字节+cache16字节)。如下图所示中的0x100008110就是bits的首地址:
四.获取bits里面的methodlists
使用objc4-838.1源码调试。参考下图实际调试效果,创建类对象HGPerson的实例对象p,通过x/6gx p.class查看类对象HGPerson的内存分布情况。类对象首地址通过内存平移32字节即图中0x100008270为bits的首地址。已知bits是class_data_bits_t类型的,通过类型转换得出class_data_bits_t类型的$2,读取$2得到$3。
找到class_data_bits_t的源码,发现其中有data函数且返回class_rw_t类型数据,通过读取$3的data函数得到$4。读取$4得到class_rw_t类型的$5。class_rw_t类型有methods函数且返回method_array_t类型数据.通过读取$5的method函数得到method_array_t类型的$6,$6为列表,通过$6.list读取得到$7,读取$7.ptr得到$8,再读取$8。
读取$8得到method_list_t类型的$9,其中数据中count = 6即为类对象中方法的个数。method_list_t继承自entsize_list_tt。entsize_list_tt可当成为一个容器(或模版),其中有代表元素的tvpename Element参数和容器类型的typename List参数,所以通过struct method_list_t : entsize_list_tt‹method_t, method_list_t, oxffff0003, method_t: :pointer_modifier> {} 可知,其元素类型为method_t,容器类型为method_list_t。所以可以通过某个方法获取其中的元素,这个方法为get方法,
接着发现通过get方法无法获取到数据,分析查看源码发现,元素类型method_t中的big包含SEL name、types、MethodListIMP imp的数据。由查找函数发现big(小端时getDescription)函数返回此数据。从而获取到类对象中的所有方法数据信息。
知识小亮点
1.大端:高位字节存放内存的低地址段,低位字节存放内存的高地址段;
2.小端:高位字节存放内存的高地址段,低位字节存放内存的低地址段;
例如:0x12345678 0x10001 0x10002 0x10003 0x10004
大端: 0x12 0x34 0x56 0x78
小端: 0x78 0x56 0x34 0x12
复制代码
五.获取bits里面的propertylist(属性)
与四.获取bits里面的methodlists同理,获取bits里面的propertylist。propertylist函数的返回类型为property_array_t,容器property_array_t中的元素为property_t类型,property_t结构体含有name和attributes元素。最终获取到bits里面的propertylist有name和attributes这2个!
注意⚠️:指针类型调用函数使用"->"。
复制代码
遗留问题
1.观察发现,成员变量_hobby没有在属性列表中展示,`后续章节`进行研究!
2.类方法存储在哪?
复制代码
本文总结
- 类对象有且只有一个。实例对象 isa->类对象 isa->元类 isa->根元类 isa-> 根元类本身,NSObject isa->根元类;
- 根元类的父类为NSObject,父类的元类就是元类的父类;
- objc_class 存储实例方法、属性、协议;
- 本文详细追踪并获取bits里面的
methodlists和propertylist。