之前我们研究了对象创建流程,对象内存对齐算法解析和对象的本质,今天我们开始对类的本质进行探究。
在对象的本质文章中,通过.cpp文件其实已经对一个类所包含的内容有了初步了解。现在创建一个类,包含属性,成员变量,实例方法和类方法,我今天将带大家通过源码和lldb的方式来找到他们在类中储存的位置,并尝试取出来!

类的本质
既然研究的是类,那么类 - Class的本质是什么,是什么结构,包含什么内容呢?之前我也介绍过NSObject对应底层就是objc_object, Class对应底层为objc_class。直接在源码中搜索objc_class找到他的定义可以得知:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
- Class/objc_class 是结构体;
- objc_class继承自objc_object(万物皆对象),并继承了隐藏属性isa;
- 成员变量包括:类的isa,指向父类的指针superclass, cache和bits;
通过objc_class中成员变量的类型名称和注释,我们可以得出cache_t类型就是类的缓存数据,、class_data_bits_t是什么?注释中的class_rw_t又是什么?objc_class定义向下找到了class_rw_t:
class_rw_t *data() const {
return bits.data();
}
进入class_rw_t查看定义,发现他是一个结构体,成员变量翻到下面看到了我们熟悉的内容:
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
得出结论:类包含的属性变量,方法存在class_data_bits_t bits中。
获取类中的bits
有了目标后,我们就要先想办法把bits取出来才能具体研究bits中的data。工程里创建一个FCPerson对象,打上断点,跑起来,然后开始使用lldb:
- 使用
x/4gx FCPerson.class打印出FCPerson的内容:
image.png
可以知道输出的结果就是objc_class的成员变量,但是对照着objc_class的定义,如何取出bits呢?
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
目前我们已有的是FCPerson的首地址,尝试通过内存平移的方法找到bits。已知的是objc_class中isa所占字节为8,Class superclass所占字节为8,cache_t所占多少字节呢�?进去看cache_t的源码,去掉无用代码后如下:
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
}
可以看到cache_t为结构体,第一个成员变量explicit_atomic<uintptr_t> _bucketsAndMaybeMask大小为8字节(typedef unsigned long uintptr_t;),第二个成员变量是一个联合体,explicit_atomic<mask_t> _maybeMask占4字节(typedef uint32_t mask_t),_flags和_occupied分别占2字节,explicit_atomic<preopt_cache_t *> _originalPreoptCache;占8字节(preopt_cache_t *是指针类型)。
得出结论:cache_t所占大小为16字节。
接下来,我们通过类首地址0x00000001000042a8进行平移8(isa) + 8(superClass) + 16(cache)得到:

0x00000001000042f0是不是bits呢?我们先把他强转一下:
查看
class_data_bits_t定义,可以看到他包含着关于class_rw_t* data()的方法:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
ASSERT(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
// Set during realization or construction only. No locking needed.
// Use a store-release fence because there may be concurrent
// readers of data and data's contents.
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}
我们尝试调用一下:

调用成功,得到了
class_rw_t类型的结果!并且还提示了我们调用方法要使用->,强迫症的我就重新来一遍:
至此,成功取出了bits并且得到了
class_rw_t类型的数据集合!
在class_rw_t中找出属性
重新拿出刚刚在class_rw_t定义中看到的内容:
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
先从属性列表properties来测试:

可以看到
property_array_t为list_array_tt类型,其中的property_t定义为:
struct property_t {
const char *name;
const char *attributes;
};
property_t就是我们最终要获取到的属性,先取出list:

(同样的命令
p &17.list居然时好时坏-。-)
取出list中的ptr得到一个property_list_t *const数组:

使用*输出property_list_t的内容(隔了一天,23 -。-):

得到了新类型的数据,并且还看到
entsize_list_tt中count = 2。进入
entsize_list_tt的定义看到他是一个结构体,其中包含了一个get方法:
Element& get(uint32_t i) const {
ASSERT(i < count);
return getOrEnd(i);
}
Element& getOrEnd(uint32_t i) const {
ASSERT(i <= count);
return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
}
尝试调用get方法:

得到了name属性!因为已知
count = 2所以再次调用得到age:
再次调用get(2)会怎样?数组越界咯,大家自己去试~
至此属性的获取已经完成,接下来用同样的方法获取方法!
在class_rw_t中找出方法:

采用同样的方法获取method发现问题:对
entsize_list_tt使用get()方法时拿到的方法都是空的。跟属性相同,我们要获取的目标为
method_t,对比method_t和property_t的源码会发现method的成员变量不在表层,截取核心代码如下:
struct method_t {
method_t(const method_t &other) = delete;
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
big &big() const {
ASSERT(!isSmall());
return *(struct big *)this;
}
}
由源码可得在method_t中的big结构体才是我们需要的结果,我们来试一试:

至此,得到了属性的setter,getter方法,我们定义的
instanceMethod方法,和一个.cxx_destruct方法,经查阅资料得知.cxx_destruct方法是在ARC模式下,将所有的成员变量变成nil相当于MRC模式下的dealloc,同时在init方法中也找到了.cxx_destruct的赋值:
void sel_init(size_t selrefCount)
{
#if SUPPORT_PREOPT
if (PrintPreopt) {
_objc_inform("PREOPTIMIZATION: using dyld selector opt");
}
#endif
namedSelectors.init((unsigned)selrefCount);
// Register selectors used by libobjc
mutex_locker_t lock(selLock);
SEL_cxx_construct = sel_registerNameNoLock(".cxx_construct", NO);
SEL_cxx_destruct = sel_registerNameNoLock(".cxx_destruct", NO);
}
获取成员变量和类方法
至此我们已经获取了属性和实例方法,但是属性中没有看到成员变量,方法列表里也没有类方法,我们来继续探究,先来成员变量:
抄近路:在我查看property和method区别时,看到了ivar_t的定义,这应该就是成员变量,我通过搜索ivar_t层层倒推:ivar_t->ivar_list_t->class_ro_t,最终在class_rw_ext_t中找到了class_ro_t:
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
知道了寻找路径就很容易啦~:

成功获取成员变量hobby~
最后还剩类方法,按照class_rw_ext_t的定义来看,类方法应该只可能在methods里面,我重新阅读了一遍methods的源码以及method_t的相关定义,都没有找到类方法。结合isa的走位图和之前看过的一句话:类方法是父类的实例方法,我就想利用获取methods的流程对FCPerson的父类NSObject走一遍,看看能不能找到,结果:

这个count很明显不对,看来在NSObject中找是不靠谱的。。后来我又想到,isa的走位,在走到根类之前不是应该先走元类吗,我就尝试着获取FCPerson的metaClass:

虽然metaClass打印出来仍然显示的是FCPerson,但是从首地址判断显然跟
x/4gx FCPerson.class得到的首地址是不同的,所以我们拿到的就是FCPerson的metaClass。接下来我们利用之前的步骤向下探索,最终得到了FCPerson的类方法:

同样的命令,有时候不行,得多试几次,最后p *这一步失败过,差点以为方向出错:

至此,我们已经通过源码和lldb获取到了一个类的属性(properties中),成员变量(ro中),实例方法(methods中)以及类方法(metaClass的methods中)。
