一,结构体题对齐原则
- 结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员后面补充字节
- 结构体变量的起始地址能够被其最宽的成员大小整除,如果不能则在前一个成员后面补充字节
- 结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节
验证一下对齐原则
struct structA {
double a;//[0-7]
char b;//[8]
int c;//原则1[9-11]要补齐,所以12开始[12,13,14,15]
//原则3 成员最大8字节 (a,b,c最宽a为8)16刚好是8倍数 sizeofA-----16
};
struct structB {
char a;//[0]
struct structA b;//原则2 [1-7]要补齐,所以8开始[8-23]
int c;//[24-27] 原则3,(a,b,c最宽b为8)[28-31]要补齐 sizeofB-----32
};
输出验证
struct structA a;
NSLog(@"sizeofA-----%zu",sizeof(a));
struct structB b;
NSLog(@"sizeofB-----%zu",sizeof(b));
输出日志
2021-07-20 14:56:51.861184+0800 xzwblue[6728:183043] sizeofA-----16
2021-07-20 14:56:51.861613+0800 xzwblue[6728:183043] sizeofB-----32
对于结构体的计算只需要完全理解对齐三原则基本ok
基本数据类型内存
C | OC | 32位 | 64位 |
---|---|---|---|
bool | BOOL(64位) | 1 | 1 |
signed char | (__signed char)int8_t、BOOL(32位) | 1 | 1 |
unsigned char | Boolean | 1 | 1 |
short | int16_t | 2 | 2 |
unsigned short | unichar | 2 | 2 |
int 、int32_t | NSInteger(32位)、boolean_t(32位) | 4 | 4 |
unsigned int | boolean_t(64位)、NSUInteger(32位) | 4 | 4 |
long | NSInteger(64位) | 4 | 8 |
unsigned long | NSUInteger(64位) | 4 | 8 |
long long | int64_t | 8 | 8 |
float | CGFloat(32位) | 4 | 4 |
double | CGFloat(64位) | 8 | 8 |
二,开辟内存
- 新建对象
@interface NBPerson : NSObject
@property (nonatomic, assign) char flag;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
//@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) float height;
@end
- 输出测试
#import <objc/runtime.h>
#import <malloc/malloc.h>
NBPerson *nb = [[NBPerson alloc] init];
NSLog(@"对象类型的内存大小--%lu",sizeof(nb));
NSLog(@"对象占用内存大小--%lu",class_getInstanceSize([nb class]));
NSLog(@"系统分配的内存大小--%lu",malloc_size((__bridge const void *)(nb)));
//输出结果
2021-07-20 16:32:36.045471+0800 xzwblue[8458:248752] 对象类型的内存大小--8
2021-07-20 16:32:36.045907+0800 xzwblue[8458:248752] 对象占用内存大小--40
2021-07-20 16:32:36.046018+0800 xzwblue[8458:248752] 系统分配的内存大小--48
- sizeof 是一个运算符,并不是一个函数。 sizeof 传进来的是类型,用来计算这个类型占多大内存,这个在 编译器编译阶段 就会确定大小并直接转化成 8 、16 、24 这样的常数,而不是在运行时计算。参数可以是数组、指针、类型、对象、结构体、函数等
它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。
由于在编译时确定占用内存大小,因此sizeof不能用来返回动态分配的内存空间的大小。
nb被当作一个指针,指针类型内存大小为8
- class_getInstanceSize 用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小
依赖于<objc/runtime.h>,返回创建一个实例对象所需内存大小
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
// 返回一个类的ivar(成员变量)所占空间的大小
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
# define WORD_MASK 7UL
// 计算占用内存大小算法 (8字节对齐)
static inline uint32_t word_align(uint32_t x) {
// (8 + 7) & ~ 7
return (x + WORD_MASK) & ~WORD_MASK;
}
- malloc_size 查看源码 计算对象实际分配的内存大小,这个是由系统完成的(采用16字节对齐),
依赖于<malloc/malloc.h>,返回系统实际分配的内存大小
malloc源码分析中的segregated_size_to_fit
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
- 内存优化
- class_getInstanceSize和结构体内存计算是一样的都会遵守内存对齐原则
- 更骚的是还有优化功能,也就是结构体的内存计算会被成员顺序影响,对象的内存计算,不管你属性是怎么样排列的,系统都会按照最优的顺序,将数据存储到内存中去。
- 自己计算 上面这些都弄懂之后我们自己就可以计算内存了
@interface NBPerson : NSObject
@property (nonatomic, assign) char flag; /1
@property (nonatomic, copy) NSString *name;/8
@property (nonatomic, copy) NSString *nickName;/8
//@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *sex;/8
@property (nonatomic, assign) float height;/4
最优顺序计算
8(isa)+3*8+(4+1)8 = 40
class_getInstanceSize = 40
16字节对齐 malloc_size = 48
三,验证优化
@property (nonatomic, assign) char flag;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
//@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) float height;
NBPerson *nb = [[NBPerson alloc] init];
nb.flag = 'a';
nb.name = @"牛逼大佬";
nb.nickName = @"真牛逼";
nb.sex = @"男";
nb.height = 180.3;
(lldb) x/6gx nb
0x600001940ff0: 0x000000010398cca0 0x43344ccd00000061
0x600001941000: 0x00000001039859d8 0x00000001039859f8
0x600001941010: 0x0000000103985a18 0x0000000000000000
//很直观的
0x000000010398cca0 --- isa
0x0000000000000000 --- 内存补齐
//这个不是8字节数据
(lldb) po 0x43344ccd00000061
4842579942682132577
//类型不对
(lldb) po 0x43344ccd
1127501005
(lldb) po/f 0x43344ccd
180.300003
(lldb) po/c 0x00000061
a\0\0\0
(lldb) po 0x00000001039859d8
牛逼大佬
(lldb) po 0x00000001039859f8
真牛逼
(lldb) po 0x0000000103985a18
男
一顿操作之后发现这个char和float虽然在顺序上不在一起,但是系统会自动最优化,把他们放到一起,不得不说,的确有过人之处
四,问题探索
- 结构体为什么要字节对齐?
字节对齐的作用不仅是便于cpu快速访问,同时合理的利用字节对齐可以有效地节省存储空间
- 什么是实例对齐?
简单的说就是在开辟一块内存空间的时候,只能开辟16的整数倍字节大小的内存空间。这样以16位为单位长度开辟空间,就可以保证内存的连续性上的整齐度。
- 为什么要16字节对齐?
大家都知道字节是内存的容量单位,1内存你也可以叫它1字节。但是,CPU在读取内存的时候,却不是以字节为单位来读取的,而是以“块”为单位读取的,所以大家也经常听到一块内存,“块”的大小也就是内存存取的力度。
如果不对齐的话,在我们频繁的存取内存的时候,CPU就需要花费大量的精力去分辨你要读取多少字节,这就会造成CPU的效率低下,如果想要CPU又有效又不减少存取次数的话,那就需要找一个规范,这个规范就是字节对齐。
苹果采取16字节对齐,是因为OC的对象中,第一位叫`isa`指针,它是必然存在的,而且它就占了8位字节,就算你的对象中没有其他的属性了,也一定有一个`isa`,那对象就至少要占用8位字节。如果以8位字节对齐的话,如果连续的两块内存都是没有属性的对象,那么它们的内存空间就会完全的挨在一起,是容易混乱的。
以16字节为一块,这就保证了CPU在读取的时候,按照块读取就可以,效率更高,同时还不容易混乱。