作者:穿靴子的树
链接:https://juejin.cn/post/6912359669704949767
首先到苹果objc源码官网下载一个最新的包,这里下载的是 objc4-781.tar
Class 和实例的数据结构
看一个例子
// ---------------- Animal ----------------
@interface Animal : NSObject {
@public
int _age;
}
- (void)run; // 实例方法
+ (void)animalClassRun; // 类方法
@end
@implementation Animal
- (void)run {
// ...
}
+ (void)animalClassRun {
// ...
}
@end
// ---------------- Cat ----------------
@interface Cat : Animal {
@public
int _legs;
}
- (void)jump; // 实例方法
@end
@implementation Cat
- (void)jump {
// ...
}
@end
继承关系是: Cat: Animal: NSObject
测试一下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Cat *cat = [[Cat alloc] init];
cat->_age = 6;
cat->_legs = 4;
[cat run];
NSLog(@"cat %p", cat);
}
}
根据输出地址查看cat对象里面的内存数据(x/2xg 指令是查看前面16个字节的内存数据):
(lldb) x/2xg 0x1004aff60
0x1004aff60: 0x001d800100008299 0x0000000400000006
可以看到对象里面有具体的实例变量的值 0x0000000400000006
。0x00000004 和 0x00000006。那么0x001d800100008299
是什么呢?它其实是经过一些运算得到的 isa
指针地址
查看objc源码里面的实例对象的结构体 objc_object
。它是个这样的东西:
顺便找到 Class 的内部代码, 是个这样的东西:
好吧,它们内部其实都是结构体指针(可以看到 struct objc_class : objc_object
,所以类其实也是对象)。
存储的数据
实例,类,元类的关系:
实例 (通过isa指针 --->) 类 (通过isa指针 --->) 元类
对象的存储
这里有一个iOS交流圈:891 488 181 不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
当我们初始化一个 Cat Class
的实例对象的时候,对象里面其实存储了一个指向 Cat Class
的 isa
指针,之后会存储实例变量的值。也就是这样的:
isa // 指针地址,指向Cat类
6 // _age 变量
4 // _legs 变量
Class 的存储
Class 里面保存了一个指向这个class 的元类 MetaClass
的 isa
指针, 一个指向父类的 Class superclass
指针。当然里面还存储了属性列表,实例方法列表,方法缓存列表,协议列表,实例变量列表
等信息
在源码里面找一下,大概是这样一个结构:
objc_class
struct objc_class : objc_object {
// Class ISA; // 继承来的 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_rw_t *data() const {
return bits.data();
}
// .... more
// .... more
// .... more
class_rw_t
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
private:
using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t *, class_rw_ext_t *>; // 这个具体是什么不理它,不过应该是从这里面获取信息
// .... more
// .... more
// .... more
class_rw_ext_t
struct class_rw_ext_t {
const class_ro_t *ro;
method_array_t methods; // 方法列表
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
char *demangledName;
uint32_t version;
};
class_ro_t
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name; // 类的名字
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; // 成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
// .... more
// .... more
// .... more
元类 MetaClass 的存储
由于元类和类的结构体是一样的都是 Class 类型,只不过里面没有属性等之类的信息,然后里面存储的是类方法列表
。 所以它里面大致也就是这样:
Class isa; // isa 指针
Class superclass; // 父类指针
// .... more
// .... more
// .... more
isa 和 superclass 指针是来干什么的?
画了一张粗略的关系图:
其中 cat
是实例,Cat
是类, MetaCat
是对应的元类。虚线是对应的 isa
指针的指向,实线是对应的 superclass
指针的指向。
由于实例方法存储在类中,类方法存储在对应的元类中。所以 isa
和 superclass
指针的一大作用是方法的查找。比如
[cat jump];
那么 cat实例 会通过它的 isa 指针找到 类Cat,然后在实例方法列表里面查看 jump
方法的实现(当然它会先在方法缓存里面先看看是否存在)。
如果是这样的:
[cat run];
cat实例 会通过它的 isa 指针找到 类Cat,然后在方法列表里面查看 run
方法的实现,如果找不到,那么就通过 superclass 指针到父类的方法列表里面找,如果还没找到就一直找到 NSObject 里面。要是 NSObject 里面都没找到,那么就报错了(当然里面还涉及了方法转发相关)。 顺序是这样的:
Cat -> Animal -> NSObject
当然如果是下面这样的类方法调用:
[Cat animalClassRun];
那么它会到 MetaCat 元类里面查找方法的实现,查找的顺序是这样的:
MetaCat -> MetaAnimal -> MetaNSObject -> NSObject
总之方法查找就是先通过自己的 isa 指针查找方法,如果没有找到,那么再通过 superclass 指针一直到 NSObject 查找。
验证一下 isa 和 superclass 指针内存的指向
添加一个测试:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Cat *cat = [[Cat alloc] init];
cat->_age = 6;
cat->_legs = 4;
[cat run];
NSLog(@"Cat instance %p", cat);
// 测试一下不同方法的输出
Class catClass0 = [cat class];
Class catClass1 = [Cat class];
Class catClass2 = object_getClass(cat);
Class catClass3 = objc_getClass("Cat");
Class catMeta = object_getClass(catClass3);
NSLog(@"Cat class %p, %p, %p, %p", catClass0, catClass1, catClass2, catClass3);
NSLog(@"Cat Meta %p", catMeta);
Class animalClass = [Animal class];
Class animalMeta = object_getClass(animalClass);
NSLog(@"Animal class %p", animalClass);
NSLog(@"Animal Meta %p", animalMeta);
Class objClass = [NSObject class];
Class objMeta = object_getClass(objClass);
NSLog(@"NSObject Class %p", objClass);
NSLog(@"NSObject Meta %p", objMeta);
}
return 0;
}
输出:
2020-12-31 17:22:25.113922+0800 OCZ[50658:3217214] Cat instance 0x10065eb60
2020-12-31 17:22:25.114216+0800 OCZ[50658:3217214] Cat class 0x100008298, 0x100008298, 0x100008298, 0x100008298
2020-12-31 17:22:25.114266+0800 OCZ[50658:3217214] Cat Meta 0x100008270
2020-12-31 17:22:25.114318+0800 OCZ[50658:3217214] Animal class 0x100008248
2020-12-31 17:22:25.114350+0800 OCZ[50658:3217214] Animal Meta 0x100008220
2020-12-31 17:22:25.114388+0800 OCZ[50658:3217214] NSObject Class 0x7fff8e454118
2020-12-31 17:22:25.114418+0800 OCZ[50658:3217214] NSObject Meta 0x7fff8e4540f0
其中:
0x10065eb60
cat 实例内存地址
0x100008298
Cat 类地址
0x100008270
MetaCat 元类地址
0x100008248
Animal 类地址
0x100008220
MetaAnimal 元类地址
0x7fff8e454118
NSObject 类地址
0x7fff8e4540f0
MetaNSObject 元类地址
由于只需要查看内存里面的 isa 和 superclass 指针内存的地址,而且从之前的内存布局也知道它俩就在内存的开始位置,所以也就只需要查看前面16个字节的数据就好了(每个指针占用8个字节),使用指令 x/2xg
查看即可。
下面是具体的调试信息
(lldb) x/2xg 0x10065eb60
0x10065eb60: 0x001d800100008299 0x0000000400000006
(lldb) p/x 0x001d800100008299 & 0x00007ffffffffff8ULL
(unsigned long long) $0 = 0x0000000100008298
(lldb) x/4xg 0x100008298
0x100008298: 0x0000000100008270 0x0000000100008248
0x1000082a8: 0x000000010065c850 0x0002801800000003
(lldb) x/2xg 0x100008270
0x100008270: 0x00007fff8e4540f0 0x0000000100008220
(lldb) x/2xg 0x100008248
0x100008248: 0x0000000100008220 0x00007fff8e454118
(lldb) x/2xg 0x7fff8e454118
0x7fff8e454118: 0x00007fff8e4540f0 0x0000000000000000
(lldb) x/2xg 0x7fff8e4540f0
0x7fff8e4540f0: 0x00007fff8e4540f0 0x00007fff8e454118
(lldb)
通过调试 isa 和 superclass 指针的指向的确是如前面所说那样。
其中为什么执行这个操作 0x001d800100008299 & 0x00007ffffffffff8ULL
是因为实例的isa指针指向需要执行一个 & 操作 取 ISA_MASK 的值 0x00007ffffffffff8ULL(调试的时候是在Mac命令行项目)。这是具体的源码信息:
如果是在手机上面调试的的,则 ISA_MASK
的值是 __arm64__
的 # define ISA_MASK 0x0000000ffffffff8ULL
为什么要这样设计
由于在运行的时候可能会创建大量的类的实例,每个实例里面的变量的值都可以不一样,所以实例只需要存储有具体的实例变量的值
,和一个方法查找的isa指针
就好了。
而类和元类在整个代码运行的时候只要有一份就好了。毕竟不管是实例方法还是类方法它们具体的执行都是 objc_msgSend(void)
这个消息发送方法。
实例存储: isa指针, 实例变量列表的具体的值
类存储: isa指针, superclass指针, 属性列表,实例方法列表,实例变量列表,协议列表等
。
元类存储: isa指针, superclass指针, 类方法列表等
。
补充
object_getClass 的实现:
如源码所示,object_getClass
里面是通过 isa
指针的指向实现的。所以我们可以知道
object_getClass(实例对象) 得到的是对应的类
object_getClass(类) 得到的是对应的元类
objc_getClass 的实现:
如源码所示,objc_getClass
需要传入的是类的字符串名字,如果名字正确就得到对应的类。
文章到这里就结束了,你也可以私信我及时获取最新资料以及面试相关资料。如果你有什么意见和建议欢迎给我留言。