前言
之前了解到isa的结构,可以看到isa里shiftcls
存放的是类指针的值。这篇文章就来探索下类的结构。
isa走向
创建一个对象
DDAnimal *a = [[DDAnimal alloc] init];
通过LLDB调试,获取对象的isa
通过这几步操作,我们可以看到对象的isa是指向了DDAnimal。都说万物皆是对象,那我们来看看DDAnimal的isa是指向了哪里?
果然拿到类的数据了,我们先来看看类的结构是怎么样的。
一波操作,可以看出Class
就是objc_class
,而objc_class
继承自objc_object
,objc_object里面的有且仅有一个成员就是isa,那么0x0000000100008718
就是DDAnimal的isa。
继续操作,拿到类的地址里的数据,通过isa与上掩码,最终得到的数据是DDAnimal类又指向了一个DDAnimal,这是为什么呢,先不管了,我们再往下看看,这个DDAnimal的isa又是指向哪里的。
这里指向的类又不同了,这一个DDAnimal的isa指向变成了NSObject,再往下看看,这个NSObject的isa还指向了哪里。
通过上面的调试,我们看了$8打印出来的isa是指向了自己,都是0x000000010036a0f0
。再用这个isa去与上掩码得到结果仍然是一样的,这里可以看出已经形成了闭环。
这一波操作我们可以看出 对象a的isa指向了类DDAnimal
,DDAnimal的isa指向了又一个DDAnimal,第二个DDAnimal的isa指向了NSObject,NSObject的isa指向了自己。
那么问题是:对象a的isa指向了DDAnimal了,为什么DDAnimal的isa又指向了一个DDAnimal?用烂苹果(MachOView)看看有什么.
在烂苹果里搜索DDAnimal,可以看到_OBJC_CLASS_$_DDAnimal
就是我们创建的DDAnimal,发现上面还多了一个_OBJC_METACLASS_$_DDAnimal
类型的DDAnimal,可以看出系统自动给我们创建了一个metaclass类型,这个就是元类了。
同样的NSObject也有一个元类,如图:
那么我们获取的NSObject是_OBJC_CLASS_$_NSObject
还是_OBJC_METACLASS_$_NSObject
呢,再来验证一下。
可以看出NSObject类的地址和上面得到的不一样,而NSObject类的isa指向的地址和上面得到的一样,都是0x000000010036a0f0
。这里验证出来我们DDAnimal最终指向的应该是_OBJC_METACLASS_$_DDAnimal
,也是一个元类,NSObject是根类,那么_OBJC_METACLASS_$_NSObject
就叫做根元类。
总结一下,对象的
isa -> 类
,(DDAnimal)类的isa -> (DDAnimal的)元类
,元类的isa -> (NSObject)根元类
,根元类的isa -> 根元类(自己)
继承关系
这里直接用代码来看,更加清晰。
// DDAnimal实例对象
DDAnimal *object1 = [DDAnimal alloc];
// DDAnimal类
Class class = object_getClass(object1);
// DDAnimal元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
// DDCat元类
Class cMetaClass = object_getClass(DDCat.class);
Class csuperClass = class_getSuperclass(cMetaClass);
NSLog(@"%@ - %p",csuperClass,csuperClass);
// DDAnimal元类
Class aMetaClass = object_getClass(DDAnimal.class);
Class asuperClass = class_getSuperclass(aMetaClass);
NSLog(@"%@ - %p",asuperClass,asuperClass);
// 根元类 -> NSObject
Class rnsuperClass = class_getSuperclass(object_getClass(aMetaClass));
NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
// NSObject 根类
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"%@ - %p",nsuperClass,nsuperClass);
打印结果
> 0x100607420 // 实例对象
> 0x1000086e0 // 类
> 0x1000086b8 // 元类
> 0x7fff88a6bd60 // 根元类
> 0x7fff88a6bd60 // 根根元类
>
> DDAnimal - 0x1000086b8 // 元类
> NSObject - 0x7fff88a6bd60 // 根元类
> NSObject - 0x7fff88a6bd88 // 根类
> (null) - 0x0
从上半部分的打印可以印证上面所得的isa走向图。下面部分则表示了类的继承关系DDCat元类继承自DDAnimal元类
,DDAnimal元类继承自根元类
,根元类继承自NSObject
,NSObject继承nil
。
接下来看一个走势图,这是苹果官方的资料,图上的走势和指向我们都得到了验证。
类的结构
先把我们创建的类贴出来
@interface DDAnimal : NSObject{
NSInteger _weight;
NSString *sex;
}
@property (nonatomic, copy) NSString *cate; ///<
@property (nonatomic, copy) NSString *age; ///<
@property (nonatomic, assign) NSInteger height; ///<
- (void)jump;
+ (void)run;
@end
接下来查看类的源码
struct objc_class : objc_object {
// Class ISA; // isa
Class superclass; // 父类
cache_t cache; // 缓存
class_data_bits_t bits;
}
通过查看类的结构源码,这里只贴出了成员的代码,其他的代码都是一些方法,有点多,感兴趣的可以自己去看看。如上所示,主要有四个成员,不难看出,前三个成员就是存储一些其他信息,第四个成员应该是存储了当前类的信息。要怎么拿到bits
呢,这里我们就要用到内存偏移
,要内存偏移就要知道偏移多少,前面就三个成员,isa是8个字节,superclass是8个字节,那么cache有多少字节呢?
cache_t字节大小
通过查看cache_t源码,是一个200多行的结构,看到头就大,但是仔细一看,会发现里面大多都是函数方法,函数方法不占用结构体内存,除开函数方法,其他还有很多的static,存在静态区,也不占用结构体内存,那么就只剩下
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8字节
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4字节
#if __LP64__
uint16_t _flags; // 2字节
#endif
uint16_t _occupied; // 2字节
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
}
从上图不难算出cache_t的内存大小是16个字节。
结合上面的16个字节,那么总计是32个字节大小,需要偏移32个字节。
class_data_bits_t
通过查看源码可以看出bits里包含了属性、方法和协议
的值,我们尝试能不能获取到这些值。
// 获取到类的首地址
(lldb) x/4gx DDAnimal.class
0x100008740: 0x0000000100008718 0x000000010036a140
0x100008750: 0x000000010112d810 0x0001803800000003
// 内存偏移32个字节,也就是0x20
(lldb) p/x 0x100008740 + 0x20
(long) $1 = 0x0000000100008760
// 强转成class_data_bits_t
(lldb) p/x (class_data_bits_t *)0x0000000100008760
(class_data_bits_t *) $2 = 0x0000000100008760
// 查看源码得知属性和方法存在class_rw_t中,并且可以通过data()方法获取。
(lldb) p/x $2->data()
(class_rw_t *) $3 = 0x000000010112d7c0
获取到了class_rw_t
数据,接下来就可以查看属性和方法了
// 先来获取属性
(lldb) p/x $3->properties()
(const property_array_t) $4 = {
list_array_tt<property_t, property_list_t, RawPtr> = {
= {
list = {
ptr = 0x0000000100008588
}
arrayAndFlag = 0x0000000100008588
}
}
}
// 通过查看property_array_t的代码,它是继承自list_array_tt,在list_array_tt里看到元素是存储在ptr里,接着获取ptr,并打印地址里的数据
(lldb) p/x $4.list.ptr
(property_list_t *const) $5 = 0x0000000100008588
(lldb) p/x *$5
(property_list_t) $6 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 0x00000010, count = 0x00000003)
}
// count = 3,里面存了三个属性,在list_array_tt里看到了iterator迭代器,那么就可以通过get(i)的方式获取,操作走起
(lldb) p/x $6.get(0)
(property_t) $7 = (name = "cate", attributes = "T@\"NSString\",C,N,V_cate")
(lldb) p/x $6.get(1)
(property_t) $8 = (name = "age", attributes = "T@\"NSString\",C,N,V_age")
(lldb) p/x $6.get(2)
(property_t) $9 = (name = "height", attributes = "Tq,N,V_height")
从上面可以看出只获取到了属性,并没有获取到成员变量,并且rw里也没有看到成员变量。如法炮制,再获取方法。
(lldb) p/x $3->methods()
(const method_array_t) $10 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100008430
}
arrayAndFlag = 0x0000000100008430
}
}
}
(lldb) p/x $10.list.ptr
(method_list_t *const) $11 = 0x0000000100008430
(lldb) p/x *$11
(method_list_t) $12 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 0x0000001b, count = 0x00000007)
}
(lldb) p/x $12.get(0).big().name
(SEL) $13 = 0x0000000100003f36 "cate"
(lldb) p/x $12.get(1).big().name
(SEL) $14 = 0x0000000100003f3b "setCate:"
(lldb) p/x $12.get(2).big().name
(SEL) $15 = 0x00007fff7c54dbd5 "setHeight:"
(lldb) p/x $12.get(3).big().name
(SEL) $16 = 0x00007fff7c55165c "height"
(lldb) p/x $12.get(4).big().name
(SEL) $17 = 0x00007fff7c769392 "age"
(lldb) p/x $12.get(5).big().name
(SEL) $18 = 0x00007fff7c769396 "setAge:"
(lldb) p/x $12.get(6).big().name
(SEL) $19 = 0x00007fff7ce5fba8 "jump"
(lldb)
从上面可以看出这里获取到了所有的方法,除了类方法,同样我们也没有找到类方法的获取方式。
Tips:这里获取方法的方法和获取属性的有一点区别,直接使用get(),打印出来的是空数据,是因为
method_t
里自定义了方法,查看代码,了解到需要使用big()
来获取。 接下来要去查询成员变量和类方法,接着看代码,找到了class_ro_t
,在ro里找到了ivars
和baseMethods
,如法炮制,通过class_rw_t
里的ro()
获取到了class_ro_t
,在ivars
里面找到成员变量,但是在baseMethods
里还是没有看到类方法。
通过复盘可知,该找的地方都找了,那么类方法会不会不是存在类里面的呢,如果不是在类里面,还会在哪里呢?对了,类还有一个元类,我们到元类里去找找,LLDB走起。
(lldb) p/x 0x0000000100008718 & 0x00007ffffffffff8 // 拿到元类的地址
(long) $21 = 0x0000000100008718
(lldb) p/x (class_data_bits_t *)0x0000000100008738 // 这里直接在0x0000000100008718的基础上加了0x20,也就相当于是在isa的地址上加0x20
(class_data_bits_t *) $23 = 0x0000000100008738
(lldb) p/x $23->data()
(class_rw_t *) $24 = 0x000000010112d7a0
(lldb) p/x $24->methods()
(const method_array_t) $25 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x00000001000083c8
}
arrayAndFlag = 0x00000001000083c8
}
}
}
(lldb) p/x $25.list.ptr
(method_list_t *const) $26 = 0x00000001000083c8
(lldb) p/x *$26
(method_list_t) $27 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 0x0000001b, count = 0x00000001)
}
(lldb) p/x $27.get(0).big().name
(SEL) $28 = 0x00007fff7c52dabc "run"
耶思,获取到了类方法。
总结
- 对象的isa走位和继承的走位很重要,理解透彻了,会对学习底层理解底层有很大帮助。
- 类的属性和属性方法存在
类的class_rw_t
中,类的成员变量存在类的class_ro_t
中,类的类方法存在元类的class_rw_t
中。