对象是如何分配内存的?对象是如何计算内存大小的呢?对象内存分配跟什么有关?
代码分析
sizeof() 计算一个变量或者类型的大小(以字节为单位)
class_getInstanceSize 计算对象所需要的内存大小,结算结果遵循8字节对齐,其实现源码如下:
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
malloc_size 计算对象的实际大小,使用时需要引入头文件
//没有添加任何属性
@interface LNPerson : NSObject
@end
@implementation LNPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [LNPerson alloc];
NSLog(@"%lu - %lu - %lu",sizeof(person),class_getInstanceSize([LNPerson class]),malloc_size((__bridge const void *)(person)));
}
return 0;
}
打印结果:8 - 8 - 16
//添加一个属性
@interface LNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LNPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LNPerson *person = [LNPerson alloc];
NSLog(@"打印结果: %lu - %lu - %lu",sizeof(person),class_getInstanceSize([LNPerson class]),malloc_size((__bridge const void *)(person)));
}
return 0;
}
打印结果: 8 - 16 - 16
//添加两个属性
@interface LNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
@implementation LNPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LNPerson *person = [LNPerson alloc];
NSLog(@"打印结果: %lu - %lu - %lu",sizeof(person),class_getInstanceSize([LNPerson class]),malloc_size((__bridge const void *)(person)));
}
return 0;
}
打印结果: 8 - 24 - 32
这里先附上一张数据类型占用内存大小的表格:
由以上三分代码运行结果可以知道对象的内存大小跟属性有关(实际上是跟实例变量有关)。代码分析如下:
- 这里面sizeof的结果一直没变化,实际上他只是计算person这个指针的大小,而这个指针是一直不变的,在OC里面对象指针的大小为8字节(其他数据类型占用内存大小可以参考图1-1)。
- 通过class_getInstanceSize打印结果可以看出随着属性的增多,开辟一个对象所需要的内存也在增加(这里面需要注意的是属性age是int属性,如果按照对象所需要内存大小计算应该是8+8(name属性大小)+ 4(age)= 20,可是为什么结果却是24呢?实际上这是底层结构体的内存对齐遵循了8字节对齐的计算结果,关于结构体内存对齐可以参考相关资料。);
- malloc_size计算的是对象的实际内存大小,我们看到,这跟class_getInstanceSize计算出来的大小不一致,这实际上是因为OC对象内存分配时遵循了16字节对齐原则,当对象所需内存小于16字节时,还是会分配16字节;当对象所需内存大于16字节且大小刚好为16的倍数时,则按照所需大小分配;但是当对象所需内存大于16字节且大小不是16的倍数时,则分配大于所需内存大小的最小的16的倍数。
什么是内存对齐
内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。内存对齐意味将数据类型写入到内存地址时是按照它们大小切割的。简单说就是如果内存地址是n字节的倍数,那么我们说这n字节是内存对齐的,注意,这里n是2的幂,说白了,内存地址正好放下n字节的倍数,两者相除余数为零,正好整除。例如,从上面的代码分析可以看出对象person所需要的实际大小为24字节,而实际大小确是32字节,这是因为OC对象的内存是16字节对齐的。
为什么需要内存对齐
那编译器为什么要进行内存对齐呢?主要基于以下两个原因:
- 1、平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 2、性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
内存对齐遵循什么样的原则
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要成员有子成员,比如说数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。min(当前开始的位置m n))m = 9, n= 4:9 10 11 12 ,12是4的整数倍,所以从12开始存储
2、结构体作为成员:如果一个结构体里有些结构体成员,则结构体成员要从其内部最大元素的整数倍地址开始存储。(struct a里面存储struct b, b里有char、int、 double等元素,那b应该从8的整数倍开始存储。)
3、收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
举个例子:
// 64位
struct MyStruct1 {
double a;//8字节 a存储为0~7位置共8位
char b;//1字节 8是1的整数倍,所以b从8位置开始存储,共1位
int c;// 4字节 前8位已被占用,9不是4的倍数,所以得往前找最近的4的倍数12,从12位开始存储,共四位12~15
short d;//2字节 16位刚好是2的倍数,从16位开始存储,共两位16~17,可以推出struct1的所需大小为0~17共18位
}struct1;// 但是结构大小是其内部最大成员(这里的a,8字节)的整数倍,因此最小为24
struct MyStruct2 {// 同上面的推理
double a;//8 0~7
int c;// 4 8是4的倍数 8~11
char b;//1 12
short d;//2 13不是2的整数倍, 故14~15,所需大小位0~15共16位,实际大小应该为16
}struct2;
struct MyStruct3 {// 同上面的推理
double a;//8 0~7
int c;// 4 8是4的倍数 8~11
char b;//1 12
short d;//2 13不是2的整数倍, 故14~15,所需大小15
//如果一个结构体里有些结构体成员,则结构体成员要从其内部最大元素的整数倍地址开始存储。所以虽然这里myStruct2的大小为16字节,但是依然以8字节为倍数开始计算,这里16刚好是8的整数倍,所以myStruct2从16位开始,大小16位,所以应该是16~31位,总共大小32位
struct MyStruct2 myStruct2;//16
}struct3;// 32
打印这三个结构体:
NSLog(@"struct1大小:%ld",sizeof(struct1));
NSLog(@"struct2大小:%ld",sizeof(struct2));
NSLog(@"struct3大小:%ld",sizeof(struct3));
打印结果:
2021-07-25 17:12:57.383194+0800 MemoryAlignmentTest[8346:636413] struct1大小:24
2021-07-25 17:12:57.383265+0800 MemoryAlignmentTest[8346:636413] struct2大小:16
2021-07-25 17:12:57.383295+0800 MemoryAlignmentTest[8346:636413] struct3大小:32
结果是符合规则的。
备注:OC对象的底层结构是结构体,结构体大小内存分配遵循8字节对齐原则。但是OC对象在结构8字节对齐计算出的内存大小的基础上遵循16字节对齐原则。这么做应该是出于兼容性、容错的等方面的考虑,给对象留点空间,避免挤满。