我们在上篇文章了解到,实例对象中存储了isa指针以及成员变量的值,并且isa指针指向了类对象的所在。我们将在接下来的文章中对类对象的底层进行探索。
我们对以下内容进行输出发现其输出一模一样,也就是说这三种方式可以用来获取类对象。
实例对象,类对象,元类对象--isa指针走向以及继承关系
从上面的验证我们可以得出以下isa指针的走向图:
那么根元类的isa指针指向了谁?我们接着探索:
所以我们得出的最后的isa指针完整走向图为:
那么元类对象的父类是谁呢?我们对以下内容进行输出,就会得出答案:
Person类继承自NSObject类,由图中的输出可得到结果Person元类对象的父类是NSObject元类对象。xx的元类对象的父类是xx的类对象的父类的元类对象。也可以得出根元类的父类是根类。
我们将继承链和isa指针链进行整合:
类对象中有什么?
我们知道实例对象的底层是结构体objc_object,类对象的底层是结构体objc_class。
struct objc_object {
isa_t isa // 8
}
struct objc_class: objc_object {
Class superclass; // 8
cache_t cache; // 16
class_data_bits_t bits
}
typedef struct objc_object *id;
typedef struct objc_class *Class;
既然我们拿到了类对象的结构,根据我们目前已知的,这个结构体里面有个isa指针指向了元类对象以及一个指向父类的指针,再一个cache,这个后面我们会讲到,所以我们猜测这个class_data_bits_t类型的bits里面是我们需要的东西。首先我们来看看什么是内存平移。
内存平移
如图中所示,
p是个指针,指向了整形数组的首地址c,d+1则表示指针平移int类型大小的字节,即就是平移4个字节,也就是数组第一个元素的地址,以此类推。
好了,知道了什么是内存平移,我们来对这个bits进行探究。我们定义一个Person类,
运行程序,并且在main函数中打上断点,先输出Person类对象的地址分布,参考源码中
objc_class的结构(全局搜索struct objc_class),要拿到bits,需要内存平移32位,也就是说0x1000081b8就是我们要操作的地址。我们将该地址进行强转为class_data_bits_t*得到一个地址,我们通过*对地址进行取值,得到的结果对我们并没有什么参考意义,怎么办呢?
不过结果返回的class_data_bits_t类型我们可以试着对其进行探索,跳转到其定义处:
发现其有个叫做class_rw_t* data()的方法,我们试着对其进行调用并得到的地址进行取值:
好像没什么对我们有用的信息,这时候我们就跳转到class_rw_t类型的定义处,发现里面有很多方法,我们发现了几个很熟悉的方法:methods,properties,protocols等等。我们试着对其进行调用,里面有个list,我们继续对其进行解析,得到一个method_list_t类型的地址,并对其取值。
上面的输出信息里有个
count,我们猜测是实例方法的个数,我们定义的类中有两个属性,setter和getter方法加起来有4个,再加上定义的一个instanceMethod方法,总共有5个,可是这个count却有6个。别急,我们接着往下看。
我们查看method_list_t的定义,没找到有用的信息,去entsize_list_tt里面找:
这个get方法应该就是获取method的方法,给其传下标即可。我们可以试下,发现其输出结果并不是我们预想的那样。
那我们接着对method_t类型进行探索,跳转到其定义处,发现其有一个getDescription方法:
我们试着对其进行调用并对所得地址进行取值:
好耶,我们成功拿到了方法名以及参数列表。如法炮制,对剩下的五个方法进行打印:
原来多出的那个方法是这个叫
.cxx_destruct的方法,猜测是反初始化方法。
我们也可以通过上面的探索的方式获取属性列表。
所以类对象中存储了实例方法列表,属性列表以及协议方法列表以及cache和指向元类的指针以及指向父类的指针。