结构体内存对齐原理

前言:咱们都知道,在iOS开发中,咱们写的oc代码,底层都是用c++来完成的,而oc目标实质便是结构体指针,那么结构体占用内存的计算办法是什么呢,有没有什么规矩呢,下面咱们就来研讨一下。

首要,咱们看下面两个结构体,并且打印两个结构体占用的内存巨细,看看成果怎么。

struct Struct1 {
    double a;   // 8字节
    char b;     // 1字节
    int c;      // 4字节
    short d;    // 2字节
} struct1;
struct Struct2 {
    double a;   // 8字节
    int b;      // 4字节
    char c;     // 1字节
    short d;    // 2字节
} struct2;
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSLog(@"struct1 size : %lu \n struct2 size : %lu", sizeof(struct1), sizeof(struct2));
}

咱们看到,两个结构体成员类型都是相同的,仅仅次序不相同,他们占用内存是不是相同呢?看成果:

OC对象内存占用及优化

这成果真是让咱们大吃一斤!那为什么次序不同成果就不相同呢?咱们看一下

结构体内存对齐原则:

  1. 数据成员对⻬规矩:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的当地,以后每个数据成员存储的起始位置要从该成员巨细或许成员的子成员巨细(只需该成员有子成员,比如说是数组,结构体等)的整数倍开端(比如int为4字节,则要从4的整数倍地址开端存储。
  2. 结构体作为成员:假如一个结构里有某些结构体成员,则结构体成员要从其内部最大元素巨细的整数倍地址开端存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开端存储.)
  3. 收尾作业:结构体的总巨细,也便是sizeof的成果,必须是其内部最大成员的整数倍,缺乏的要补⻬。

看完了对齐原理,咱们来验证下为什么方才的成果是不相同的。

struct Struct1 {
    double a;   // 8字节  [0...7]
    char b;     // 1字节  [8]
    int c;      // 4字节  (9,10,11,[12...15]
    short d;    // 2字节  [16,17]
} struct1;  // 8字节内存对齐   18 -> 24
struct Struct2 {
    double a;   // 8字节  [0...7]
    int b;      // 4字节  [8...11]
    char c;     // 1字节  [12]
    short d;    // 2字节  (13,[14,15]
} struct2;  // 8字节内存对齐  16 -> 16

按照方才的原理,咱们看到确实是这样。接下来咱们加大难度:

struct Struct3 {
    double a;   // 8字节  [0...7]
    int b;      // 4字节  [8...11]
    char c;     // 1字节  [12]
    short d;    // 2字节  (13,[14,15]
    int e;      // 4字节  [16...19]
    struct Struct1 s1;  // 24字节  (20,21,22,23,[24...47]
}struct3; // 8字节内存对齐  48 -> 48

假如有结构体嵌套,根据上面的规矩,咱们计算struct3内存巨细应该是48字节,咱们打印下验证成果:

OC对象内存占用及优化

咱们再看一种状况

struct Struct4 {
    char a;     // 1字节  [0]
    short b;    // 2字节  [2,3]
    double c;   // 8字节  [8...15]
    int d;      // 4字节  [16...19]
} struct4;  // 8字节内存对齐   20 -> 24
struct Struct5 {
    int a;              // 4字节  [0...3]
    int b;              // 4字节  [4...7]
    struct Struct4 s4;  // 24字节 [8...31]
    short c;            // 1字节  [32]
}struct5; // 8字节内存对齐  33 -> 40
struct Struct6 {
    int a1;             // 4字节  [0...3]
    int b1;             // 4字节  [4...7]
    char a;             // 1字节  [8]
    short b;            // 2字节  [10,11]
    double c;           // 8字节  [16...23]
    int d;              // 4字节  [24...27]
    short e;            // 1字节  [28]
}struct6; // 8字节内存对齐  28 -> 32

OC对象内存占用及优化

C++结构体是能够承继的,那么struct5struct6却不相同,因为在承继的时分,能够理解成把父结构体这个小组织承继过来,他里边的内存分配方式不变,就算里边有多余的没有用到的内存,子结构体也没有权限去往里边写数据,所以他们的内存占用不同。

既然结构体承继是这样的,那么咱们试一下OC中的类呢。

OC对象内存占用及优化

OC中类实质便是结构体指针,那SJFather占16字节(isa->8字节,a->1字节,16字节对齐),SJSon承继SJFather,假如按上面结构体状况,是不是先把SJFather的16字节承继过来且没权限修正,再加上一个b->1字节,16字节对齐后占32字节。可是咱们看到打印出来16字节,也便是在底层,SJSon直接把SJFather成员变量放在自己的结构体中,并没有结构体嵌套,所以SJSon占用的内存:isa->8 + a->1 + b->1 = 10,16字节对齐后16字节,这儿需求注意下。

OC目标内存巨细

下面咱们来研讨下目标的内存巨细。

@interface SJPerson : NSObject
@property (nonatomic, copy) NSString *name;  // 8
@property (nonatomic, copy) NSString *nickName;  // 8
@property (nonatomic, assign) int age;  // 4
@property (nonatomic, assign) long height;  // 8
@end
SJPerson *sj = [[SJPerson alloc] init];
NSLog(@"%@ - %lu - %lu - %lu", sj, sizeof(sj), class_getInstanceSize([SJPerson class]), malloc_size((__bridge const void *)(sj)));

OC对象内存占用及优化

指针8字节,根据上面结构体内存,咱们可算出成员变量内存对齐后占用28 -> 32字节,加上isa指针8字节,共40字节,SJPerson这个类占用40字节就够了,为什么malloc_size打印出来是48呢,咱们研讨下。

malloc 流程

找到malloc源码,看下calloc流程有哪些。

  1. _malloc_zone_calloc
void *
calloc(size_t num_items, size_t size)
{
	return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
  1. 咱们根据回来值ptr,找到关键信息zone->calloc

OC对象内存占用及优化

可是咱们点calloc进去

void 	*(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */

什么信息都看不到,咱们看源码calloc有许多calloc = xxx赋值的当地,有赋值的当地就有存储值的当地。 咱们能够在zone->calloc打个断点,当执行到这行代码时,在控制台po zone->calloc,就会发现输出default_zone_calloc,咱们在大局查找。 或许用汇编,也能够看到走到default_zone_calloc办法。 3. default_zone_calloc

static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
	zone = runtime_default_zone();
	return zone->calloc(zone, num_items, size);
}

回来值同样看不到任何信息,咱们打断点故技重施,会输出nano_calloc 4. nano_calloc

static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
	size_t total_bytes;
        /// 回来null不必看,咱们肯定要找成功回来
	if (calloc_get_size(num_items, size, 0, &total_bytes)) {
		return NULL;
	}
	if (total_bytes <= NANO_MAX_SIZE) {
                /// 重要信息
		void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
		if (p) {
			return p;
		} else {
			/* FALLTHROUGH to helper zone */
		}
	}
        /// 当total_bytes大于256,执行下面代码,需求再验证下
	malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
	return zone->calloc(zone, 1, total_bytes);
}
  1. _nano_malloc_check_clear

OC对象内存占用及优化

找到最关键的代码,segregated_next_block便是死循环查找合适内存空间。

  1. segregated_next_block

OC对象内存占用及优化

总结下calloc流程图如下:

OC对象内存占用及优化

calloc流程基本走完了,可是咱们最关怀的问题,请求空间请求多大呢?咱们再回到第5步中,slot_bytes这个字段即拓荒内存空间巨细。再网上看这个值咋么获取的

size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);
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 = 16
		size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
	}
	/// (size + 15) >> 4 << 4,即k为大于size的最小的16字节对齐数据
	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是40的时分,malloc_size是48了,因为要16字节对齐。那为什么要以16字节对齐呢,oc中成员变量最多的占8字节,或许说为什么不以32字节对齐或许其他。因为假如以8字节对齐,不同目标内存空间是接连挨在一起的,访问时有可能会产生过错,也便是野指针访问,假如扩大到16,内存接连的可能性会下降,一个NSObject目标只要一个isa指针,占8字节,空8字节,产生访问过错的几率就会下降,并且随便加一个成员变量,内存就会大于8,假如以8字节对齐,计算量会变大。为什么不必更大32呢?32的话可能会浪费许多内存,所以综上考虑,iOS目标内存空间用16字节对齐。

OC目标内存优化

OC对象内存占用及优化
打印看下sj的内存分配

OC对象内存占用及优化

能够看出体系自动帮咱们做了内存分配优化,并且咱们写的属性的次序与内存位置次序无关。