004:类原理分析(上):类(isa、superclass、cache、bits[class_ro_t class_rw_t class_rw_ext_t])
问题
1:为什么元类是唯一的?
2: isa走位图
2: class_rw_o和class_rw_t的区别
目录
1:isa走位图
2:元类
3:isa走位图
4:OC对象的本质
5:获取属性列表
6:方法列表
7:获取成员变量
8:类方法的存储
9:类的信息是如何存储的
10:class_ro_t-->>class_rw_t
预备
1:定义两个类
1.1:继承自NSObject
的类CJLPerson
,
@interface CJLPerson : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end
@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end
1.2:继承自CJLPerson
的类CJLTeacher
@interface CJLTeacher : CJLPerson
@end
@implementation CJLTeacher
@end
1.3:在main中分别用两个定义两个对象:person & teacher
int main(int argc, const char * argv[]) {
@autoreleasepool {
//ISA_MASK 0x00007ffffffffff8ULL
CJLPerson *person = [CJLPerson alloc];
CJLTeacher *teacher = [CJLTeacher alloc];
NSLog(@"Hello, World! %@ - %@",person,teacher);
}
return 0;
}
正文
1:isa走位图
获取isa中类的信息
获取isa指针-->>获取shiftcls(里面存储类的信息)
最上面打印类的信息,中间打印类信息的三种方式,下面是获取类的isa指针指向的元类信息
根据调试过程,我们产生了一个疑问
:为什么图中的p/x 0x001d8001000022dd & 0x00007ffffffffff8ULL
与 p/x 0x00000001000022b0 & 0x00007ffffffffff8ULL
中的类信息打印出来都是CJLPerson
?
0x001d8001000022dd
是person
对象的isa指针地址
,其&
后得到的结果
是创建person
的类CJLPerson
0x00000001000022b0
是isa中获取的类信息所指的类的isa
的指针地址,即CJLPerson类的类
的isa
指针地址,在Apple中,我们简称CJLPerson类的类
为元类
- 所以,两个打印都是
CJLPerson
的根本原因就是因为元类
导致的
2:元类
1:下面来解释什么是元类
,主要有以下几点说明:
-
我们都知道
对象
的isa
是指向类
,类
的其实也是一个对象
,可以称为类对象
,其isa
的位域指向苹果定义的元类
-
元类
是系统
给的,其定义
和创建
都是由编译器
完成,在这个过程中,类
的归属
来自于元类
-
元类
是类对象
的类
,每个类
都有一个独一无二的元类
用来存储类方法的相关信息
。 -
元类
本身是没有名称的
,由于与类
相关联
,所以使用了同类名一样的名称
下面通过lldb
命令来探索元类的走向
,也就是isa
的走位
,如下图所示,可以得出一个关系链:对象 --> 类 --> 元类 --> NSobject, NSObject 指向自身
对象
的isa
指向类
(也可称为类对象
)类
的isa
指向元类
元类
的isa
指向根元类
,即NSObject
根元类
的isa
指向 它自己
2:从图中可以看出,最后的根元类
是NSObject
,这个NSObject
与我们日开开发中所知道的NSObject
是同一个吗?是同一个。
两种验证方式:
lldb
命令验证
代码
验证
2.1:lldb
命令验证
NSObject
类的元类
也是NSObject
,与上面的CJLPerson
中的根元类
(NSObject)的元类
,是同一个,所以可以得出一个结论:内存中只存在存在一份根元类NSObject,根元类的元类是指向它自己
2.2:代码
验证
//MARK:--- 分析类对象内存 存在个数 void testClassNum(){ Class class1 = [CJLPerson class]; Class class2 = [CJLPerson alloc].class; Class class3 = object_getClass([CJLPerson alloc]); NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3); }
从结果中可以看出,打印的地址都是同一个
,所以NSObject只有一份
,即NSObject(根元类)
在内存中永远只存在一份
类的信息在内存中永远只存在一份,所以 类对象只有一份
3:isa走位图
isa走位
isa的走向有以下几点说明:
-
实例对象(Instance of Subclass)
的isa
指向类(class)
-
类对象(class)
isa
指向元类(Meta class)
-
元类(Meta class)
的isa
指向根元类(Root metal class)
-
根元类(Root metal class)
的isa
指向它自己
本身,形成闭环
,这里的根元类
就是NSObject
superclass走位
superclass(即继承关系)的走向也有以下几点说明:
类
之间 的继承
关系:-
类(subClass)
继承自父类(superClass)
-
父类(superClass)
继承自根类(RootClass)
,此时的根类是指NSObject
-
根类
继承自nil
,所以根类
即NSObject
可以理解为万物起源
,即无中生有
-
元类
也存在继承
,元类之间的继承关系如下:-
子类的元类(metal SubClass)
继承自父类的元类(metal SuperClass)
-
父类的元类(metal SuperClass)
继承自根元类(Root metal Class
-
根元类(Root metal Class)
继承于根类(Root class)
,此时的根类是指NSObject
-
- 【注意】
实例对象
之间没有继承关系
,类
之间有继承关系
CJLTeacher及对象teacher
、CJLPerson及对象person
举例说明,如下图所示-
isa 走位链(两条)
-
teacher的isa走位链:
teacher(子类对象) --> CJLTeacher (子类)--> CJLTeacher(子元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)
-
person的isa走位图:
person(父类对象) --> CJLPerson (父类)--> CJLPerson(父元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)
-
-
superclass走位链(两条)
-
类的继承关系链:
CJLTeacher(子类) --> CJLPerson(父类) --> NSObject(根类)--> nil
-
元类的继承关系链:
CJLTeacher(子元类) --> CJLPerson(父元类) --> NSObject(根元类)--> NSObject(根类)--> nil
-
4:OC对象的本质
1:objc_class & objc_object
struct objc_class : objc_object { // Class ISA; // 8字节 Class superclass; // 8字节 cache_t cache; // formerly cache pointer and vtable, 16字节 class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags。。首地址右移32字节 class_rw_t *data() const { return bits.data(); } void setData(class_rw_t *newData) { bits.setData(newData); } } struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; };
isa
属性:继承自objc_object
的isa
,占 8
字节superclass
属性:Class
类型,Class
是由objc_object
定义的,是一个指针
,占8
字节struct cache_t { #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节 explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节 #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节 mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节 #if __LP64__ uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节 #endif uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
-
计算
前两个属性
的内存大小
,有以下两种情况,最后的内存大小总和都是12
字节-
【情况一】
if
流程-
buckets
类型是struct bucket_t *
,是结构体指针
类型,占8
字节 -
mask
是mask_t
类型,而mask_t
是unsigned int
的别名,占4
字节
-
-
【情况二】
elseif
流程-
_maskAndBuckets
是uintptr_t
类型,它是一个指针
,占8
字节 -
_mask_unused
是mask_t
类型,而mask_t
是uint32_t
类型定义的别名,占4
字节
-
-
-
_flags
是uint16_t
类型,uint16_t是unsigned short
的别名,占2
个字节 -
_occupied
是uint16_t
类型,uint16_t是unsigned short
的别名,占2
个字节
总结:所以最后计算出cache类的内存大小 = 12 + 2 + 2 = 16
字节
3:获取bits,获取联合体位域的地址
想要获取bits
的中的内容,只需通过类
的首地址平移32
字节即可
4:获取类的首地址有两种方式
p/x CJLPerson.class
直接获取首地址
x/4gx CJLPerson.class
,打印内存信息获取
5:获取类的信息
其中的data()
获取数据,是由objc_class
提供的方法
$2
指针的打印结果中可以看出bits
中存储的信息,其类型是class_rw_t
,也是一个结构体类型。但我们还是没有看到属性列表、方法列表
等,需要继续往下探索
struct 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 *>()->methods; } else { return method_array_t{v.get<const class_ro_t *>()->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 *>()->properties; } else { return property_array_t{v.get<const class_ro_t *>()->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 *>()->protocols; } else { return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols}; } } }
在这个 class_rw_t
结构体中我们发现这里有methods(方法)、properties(属性)、protocols(协议)
这些信息
5:获取属性列表
在获取bits
并打印bits
信息的基础上,通过class_rw_t
提供的方法,继续探索 bits
中的属性列表
,以下是lldb 探索的过程图示
6:探索 方法列表,即methods_list
通过查看objc_class
中bits
属性中存储数据的类class_rw_t
的定义发现,除了methods、properties、protocols
方法
7:获取成员变量
还有一个ro
方法,其返回类型是class_ro_t
,通过查看其定义,发现其中有一个ivars
属性。
lass_ro_t
结构体中的属性如下所示,想要获取ivars
,需要ro的首地址平移48
字节
class_ro_t
结构体中的属性如下所示,想要获取ivars
,需要ro的首地址平移48
字节
struct class_ro_t { uint32_t flags; //4 uint32_t instanceStart;//4 uint32_t instanceSize;//4 #ifdef __LP64__ uint32_t reserved; //4 #endif const uint8_t * ivarLayout; //8 const char * name; //1 ? 8 method_list_t * baseMethodList; // 8 protocol_list_t * baseProtocols; // 8 const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; //方法省略 }
8:探索类方法的存储
由此可得出methods list 中只有 实例方法,没有类方法
,那么问题来了,类方法存储在哪里?为什么会有这种情况?下面我们来仔细分析下
在文章前半部分,我们曾提及了元类
,类对象
的isa
指向就是元类
,元类
是用来存储类
的相关信息的,所以我们猜测:是否类方法存储在元类的bits中呢
?可以通过lldb
命令来验证我们的猜测
通过图中元类方法列表
的打印结果,我们可以知道,我们的猜测是正确的,所以可以得出以下结论:
-
类
的实例方法
存储在类的bits属性
中,通过bits --> methods() --> list
获取实例方法列表
,例如CJLPersong
类的实例方法sayHello
就存储在CJLPerson类的bits
属性中,类中的方法列表
除了包括实例方法
,还包括属性的set方法
和get方法
-
类
的类方法
存储在元类的bits属性
中,通过元类bits --> methods() --> list
获取类方法列表
,例如CJLPerson
中的类方法sayBye
就存储在CJLPerson
类的元类
(名称也是CJLPerson)的bits
属性
9: 类的信息是如何存储的
1: class_rw_t
存储着 class_ro_t
和 methods
(方法列表)、properties
(属性列表)、protocols
(协议列表)等信息。
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; // 省略过多的代码 ... 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 *>()->methods; } else { return method_array_t{v.get<const class_ro_t *>()->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 *>()->properties; } else { return property_array_t{v.get<const class_ro_t *>()->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 *>()->protocols; } else { return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols}; } } };
2:class_ro_t
中也存储了 baseMethodList
(方法列表)、baseProperties
(属性列表)、baseProtocols
(协议列表) 以及 实例变量、类的名称、大小 等等信息。
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; };
10: class_ro_t-->>class_rw_t
在类首次被使用的时候,runtime会为它分配额外的存储容量,用于 读取/写入 数据的一个结构体 class_rw_t
。
class_ro_t
是只读的,存放的是 编译期间就确定的字段信息;而class_rw_t
是在 runtime 时才创建的,它会先将 class_ro_t
的内容拷贝一份,再将类的分类的属性、方法、协议等信息添加进去,之所以要这么设计是因为 Objective-C 是动态语言,你可以在运行时更改它们方法,属性等,并且分类可以在不改变类设计的前提下,将新方法添加到类中因为 class_ro_t
是只读的,所以我们需要在 class_rw_t
中追踪这些东西。而这样做,显然是会占用相当多的内存的。
事实证明,class_rw_t
会占用比 class_ro_t
占用更多的内存,在Apple的测试中,iPhone在系统中大约有 30MB 的 class_rw_t
结构。应该如何优化这些内存呢?
通过测量实际设备上的使用情况,大约只有 10% 的类实际会存在动态的更改行为(如动态添加方法,使用 Category 方法等)。因此,苹果的工程师 把这些动态的部分提取了出来单独放在一个区域,我们称之为 class_rw_ext_t
,这样的设计使得 class_rw_t
的大小减少了一半, 所以,结构会变成这个样子。
注意