前面我们已经学习了对象相关内容, 如元类
,根元类
,以及superclass
的结构与关系等;初步分析了类的结构superclass
,cache
,class_data_bits_t
,class_data_bits_t
结构体中提供了data()方法
,用于获取class_rw_t
,class_rw_t
是在类初始化过程中已经被创建了,并且class_rw_t
的相关数据来自MachO文件
中ro
数据!简单总结:对象是类的实例,类是元类的实例,方法都存储在各自的类中。
一. iOS类属性重排
1、自定义NYPerson类,不定义任何成员变量和属性
NYPerson *person = [[NYPerson alloc] init];
NSLog(@"person对象类型占用内存的大小:%lu",sizeof(person));
NSLog(@"person对象实际占用的内存的大小:%lu",class_getInstanceSize([person class]));
NSLog(@"person对象实际分配型内存的大小:%lu",malloc_size((__bridge const void*)(person)));
**2022-04-30 20:15:34.888166+0800 Test[3543:74546] person对象类型占用内存的大小:8**
**2022-04-30 20:15:34.888436+0800 Test[3543:74546] person对象实际占用的内存的大小:8**
**2022-04-30 20:15:34.888594+0800 Test[3543:74546] person对象实际分配型内存的大小:16**
复制代码
实际占用内存大小为8是因为NYPerson继承NSObject
,NSObject自带Class 类型isa成员变量
, 本质是isa指针,占8个字节。 实际分配内存为16是因为OC对象内存开辟遵循16字节对齐。在ios对象的底层探索(下)中有详细介绍.
2、给NYPerson添加sex、heigt属性,看打印结果:
@interface NYPerson : NSObject
@property (nonatomic, assign) BOOL sex;
@property (nonatomic, assign) double height;
@end
**2022-04-30 20:28:51.680828+0800 Test[3963:85673] person对象类型占用内存的大小:8**
**2022-04-30 20:28:51.681218+0800 Test[3963:85673] person对象实际占用的内存的大小:24**
**2022-04-30 20:28:51.681941+0800 Test[3963:85673] person对象实际分配型内存的大小:32**
复制代码
person对象实际占用的内存情况: 0~7存储isa指针
,第8位存储sex,16~23存储height
,所以实际占用为8的倍数24,实际分配为16的倍数32。
3、给NYPerson添加sex、heigt属性,看打印结果:
@interface NYPerson : NSObject
@property (nonatomic, assign) BOOL sex;
@property (nonatomic, assign) double height;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) short weight;
@end
**2022-04-30 20:44:03.189658+0800 Test[4275:95130] person对象类型占用内存的大小:8**
**2022-04-30 20:44:03.191476+0800 Test[4275:95130] person对象实际占用的内存的大小:24**
**2022-04-30 20:44:03.194769+0800 Test[4275:95130] person对象实际分配型内存的大小:32**
复制代码
疑问:如果实际占用内存按照自定义属性的顺序来计算的话,那么实际占用内存应该是 0~7存储isa指针,第8位存储sex,16~23位存储height,24~27位存储age,28~29位存储weight
,然后按8字节对齐应该是32才对,但是这里还是24,这就证明编译器肯定对内存进行过优化。所以我们定义属性的时候不用管定义的顺序,编译器会自动帮我们进行优化。
小结: 编译器会对属性顺序进行优化,从而节省内存
4、给属性赋值,通过lldb调试证明
当我们向0x0000001200140001
地址找出数据时,发现是看不懂的,这里无法找出值的原因是苹果中针对sex、age、weight属性的内存进行了重排
,因为age类型占4个字节,sex和weight类型分别占1个字节,2个字节,通过4+2+1的方式,按照8字节对齐,不足补齐的方式存储在同一块内存中
。注意:浮点型需要用p/f查看
5、只有成员变量时,还会不会自动帮我们排序呢?
将属性改为成员变量
,再次打印结果会,发现编译器不会自动帮我们进行优化
,而是按照实际成员变量顺序
来计算实际占用内存的大小。
小结:使用成员变量,编译器并不会自动帮我们进行内存优化,需要自己去排序成员变量,而使用属性,编译器会自动帮我们进行内存优化,从而节约内存。
6、既有成员变量又有属性时
@interface NYPerson : NSObject
{
double sheight;
BOOL scout;
}
@property (nonatomic, assign) BOOL sex;
@property (nonatomic, assign) double height;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) short weight;
@end
**2022-04-30 21:13:41.448553+0800 Test[4968:116618] person对象类型占用内存的大小:8**
**2022-04-30 21:13:41.450152+0800 Test[4968:116618] person对象实际占用的内存的大小:32**
**2022-04-30 21:13:41.450493+0800 Test[4968:116618] person对象实际分配型内存的大小:32**
----------------------------------------------------------------------------------------
@interface NYPerson : NSObject
{
BOOL scout;
double sheight;
}
@property (nonatomic, assign) BOOL sex;
@property (nonatomic, assign) double height;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) short weight;
@end
**2022-04-30 21:14:16.863168+0800 Test[4991:117397] person对象类型占用内存的大小:8**
**2022-04-30 21:14:16.863454+0800 Test[4991:117397] person对象实际占用的内存的大小:40**
**2022-04-30 21:14:16.863842+0800 Test[4991:117397] person对象实际分配型内存的大小:48**
复制代码
小结:从上面代码可看出,当成员变量和属性同事存在时,会先给成员变量按顺序分配内存,然后再给属性自动优化分配内存。
二. 类结构class_ro_t,class_rw_t,class_rw_ext_t的解析
1、class_ro_t
class_ro_t
存储了当前类在编译期就已经确定的属性
、方法
以及遵循的协议
,里面是没有分类的方法
。那些运行时添加的方法将会存储在运行时生成的class_rw_t
中。
ro
即表示read only
,是无法进行修改的。
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;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
复制代码
2、class_rw_t
ObjC
类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t
中:
// 可读可写
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro; // 指向只读的结构体,存放类初始信息
/*
这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
methods中,存储 method_list_t ----> method_t
二维数组,method_list_t --> method_t
这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
*/
method_array_t methods; // 方法列表(类对象存放对象方法,元类对象存放类方法)
property_array_t properties; // 属性列表
protocol_array_t protocols; //协议列表
Class firstSubclass;
Class nextSiblingClass;
//...
}
复制代码
class_rw_t
生成在运行时,在编译期间,class_ro_t
结构体就已经确定,objc_class
中的bits
的data
部分存放着该结构体的地址。在runtime
运行之后,具体说来是在运行runtime
的realizeClass
方法时,会生成class_rw_t
结构体,该结构体包含了class_ro_t
,并且更新data
部分,换成class_rw_t
结构体的地址。 类的realizeClass
运行之前:
类的realizeClass
运行之后: 细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容剪切过去,然后再将当前类的分类的这些属性、方法等剪切到其中
。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容
3、class_rw_ext_t
class_rw_ext_t
可以减少内存的消耗。苹果在wwdc2020⾥⾯说过,只有⼤约10%左右的类需要动 态修改。所以只有10%左右的类⾥⾯需要⽣成class_rw_ext_t
这个结构体。这样的话,可以节约很 ⼤⼀部分内存。
class_rw_ext_t⽣成的条件:
第⼀:⽤过runtime
的Api进⾏动态修改
的时候。
第⼆:有分类
的时候,且分类
和本类
都为⾮懒加载类
的时候。实现了+load⽅法
即为⾮懒加载类。
class_ro_t
,class_rw_t
,class_rw_ext_t
,他们之间的关系图:
三.通过runtime的api探索类的数据结构
1、获取类的成员变量
-(void)ny_class_copyIvarList:(Class)pClass {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(pClass, &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
const char *cName = ivar_getName(ivar);
const char *cType = ivar_getTypeEncoding(ivar);
NSLog(@"name = %s type = %s",cName,cType);
}
free(ivars);
}
复制代码
2、获取类的属性
-(void)ny_class_copyPropertyList:(Class)pClass {
unsigned int outCount = 0;
objc_property_t *perperties = class_copyPropertyList(pClass, &outCount);
for (int i = 0; i < outCount; i++) {
objc_property_t property = perperties[i];
const char *cName = property_getName(property);
const char *cType = property_getAttributes(property);
NSLog(@"name = %s type = %s",cName,cType);
}
free(perperties);
}
复制代码
3、获取类的方法
-(void)lg_class_copyMethodList:(Class)pClass {
unsigned int outCount = 0;
Method *methods = class_copyMethodList(pClass, &outCount);
for (int i = 0; i < outCount; i++) {
Method method = methods[i];
NSString *name = NSStringFromSelector(method_getName(method));
const char *cType = method_getTypeEncoding(method);
NSLog(@"name = %@ type = %s",name,cType);
}
free(methods);
}
复制代码
总结
1.OC对象里面属性在编译时,编译器会对属性顺序进行优化,从而节省内存。而使用成员变量时,编译器会安装,我们书写的顺序进行内存计算对齐,不会进行优化。
2.class_ro_t存储了当前类在编译期就已经确定的属性
、方法
以及遵循的协议
,里面是没有分类的方法
。
2.class_rw_t是在运⾏的时候⽣成的,类⼀经使⽤就会变成class_rw_t
,它会先将class_ro_t的内 容"拿"过去,然后再将当前类的分类的这些属性
、⽅法
、协议
等拷⻉到class_rw_t⾥⾯其中class_ro_t *ro 指向class_ro_t。它是可读写的。
4.class_rw_ext_t可以减少内存的消耗。class_rw_ext_t⽣成的条件:
(1)⽤过runtime的Api进⾏动态修改的时候。
(2)有分类的时候,且分类和本类都为⾮懒加载类的时候。实现了+load⽅法即为⾮懒加载类。