之前我们研究了对象创建流程,对象内存对齐算法解析和对象的本质,今天我们开始对类的本质进行探究。
在对象的本质文章中,通过.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的内容:
可以知道输出的结果就是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中)。