OC结构体和对象内存对齐原理

454 阅读6分钟

一、对象内存的影响因素

定义一个ApplePerson类的属性

@interface ApplePerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@end

加个断点 image.png 进行打印

(lldb) po class_getInstanceSize(ApplePerson.class)
24
(lldb) 

注释一个属性

@interface ApplePerson : NSObject
@property (nonatomic, copy) NSString *name;
//@property (nonatomic, copy) NSString *nickName;
@end

再次打印

(lldb) po class_getInstanceSize(ApplePerson.class)
16
(lldb) 

变成16,这说明属性影响类对象的大小, 接下来,我们给类添加1个成员变量

@interface ApplePerson : NSObject{
    NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
//@property (nonatomic, copy) NSString *nickName;
@end

再次打印大小,又变成24

(lldb) po class_getInstanceSize(ApplePerson.class)
24
(lldb) 

说明成员变量也影响类对象的大小。 而属性和成员变量之间相差了方法,按上面的可以推测出方法是不会影响大小的,接下来我们做一下验证: 我们为ApplePerson添加一个方法声明和实现

-(void)say666;
#import "ApplePerson.h"
@implementation ApplePerson
-(void)say666{
}
@end

再次测试大小

(lldb) po class_getInstanceSize(ApplePerson.class)
24
(lldb) 

还是24,说明方法并不影响类对象的大小,所以属性影响大小只是属性的成员变成影响类对象的大小而已。 再测试一下类方法:

(lldb) po class_getInstanceSize(ApplePerson.class)
24
(lldb) 

说明成员变量也影响类对象的大小。 而属性和成员变量之间相差了方法,按上面的可以推测出方法是不会影响大小的,接下来我们做一下验证: 我们为ApplePerson添加一个方法声明和实现

+(void)say666;
#import "ApplePerson.h"
@implementation ApplePerson
+(void)say666{
}
@end

测试大小

(lldb) po class_getInstanceSize(ApplePerson.class)
24
(lldb) 

还是24,说明类方法也没有影响。

二、结构体内存对齐探究

重新写ApplePerson类的属性

@interface ApplePerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic) double height;
@property (nonatomic) char c1;
@property (nonatomic) char c2;

@end

赋值并且打断点

 p.name = @"苹果";
 p.nickName = @"烂苹果";
 p.age       = 45;
 p.height    = 183;
 p.c1        = 'c';
 p.c2        = 'd';

打印一下

(lldb) x/8gx p
0x600003ffd230: 0x01000001007b9781 0x0000002d00006463
0x600003ffd240: 0x00000001007b4028 0x00000001007b4048
0x600003ffd250: 0x4066e00000000000 0x0000000000000000
0x600003ffd260: 0x0000000000000000 0x0000000000000000
(lldb) po 0x00000001007b4028
苹果
(lldb) po 0x00000001007b4048
烂苹果
(lldb) po 0x0000002d
45
(lldb) po 0x64
100
(lldb) po 0x63
99

可以看到0x0000002d00006463是有三个属性,这就涉及到了内存对齐。 内存对齐原则: levphy.github.io/2017/03/23/…

  1. 对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍
  2. 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍
  3. 如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型 接下来看下面两个结构体:
struct AppleStruct1 {
    double a;  
    char b;    
    int c;     
    short d;    
}aStruct1;

struct AppleStruct2 {
    double a;  
    int b;     
    char c;     
    short d;    
}aStruct2;

虽然看起来结构体内部只是顺序有点不同而已,但是根据内存对齐原则计算,它们的内存大小并不一致:

struct AppleStruct1 {
    double a;   //8     [0-7]
    char b;     //1     [8]
    int c;      //4     (9 10 11)[12 13 14 15]
    short d;    //2     [16 17] (18 19 20 21 22 23) -- 最少占用24个字节
}aStruct1;

struct AppleStruct2 {
    double a;   //8     [0-7]
    int b;      //4     [8-11]
    char c;     //1     [12]
    short d;    //2     (13)[14 15](16) -- 最少占用16个字节
}aStruct2;

通过计算,我们得出aStruct1占用24个字节,而aStruct2占用16字节,接下来我们通过代码验证一下:

ApplePerson *p2 = [ApplePerson alloc];
NSLog(@"%@",p2);
NSLog(@"%lu-%lu",sizeof(aStruct1),sizeof(aStruct2));

跑一下代码

2021-07-24 00:00:03.272401+0800 001-内存对齐原则[3263:296345] <ApplePerson: 0x6000005d81b0>
2021-07-24 00:00:03.272655+0800 001-内存对齐原则[3263:296345] 24-16

验证了上面的计算 接下来看系统内存开辟分析:

@interface ApplePerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@end

通过代码计算一下:

 ApplePerson *ap = [ApplePerson alloc];
 NSLog(@"%@ - %lu - %lu -%lu",ap,sizeof(ap),class_getInstanceSize([ApplePerson class]),malloc_size((__bridge const void *)(ap)));

可得结果:

<ApplePerson: 0x1006338c0> - 8 - 40 - 48

28 内存对齐 = 32 + isa指针8 = 40 再malloc_size对齐 = 48 接下来就是malloc:

三、malloc源码引入探索

我们编译苹果的libmalloc-317.40.8源码

image.png

给main方法添加断点 image.png 点击calloc方法,进去

void *
calloc(size_t num_items, size_t size)
{
	return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}

点击_malloc_zone_calloc进去

static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
		malloc_zone_options_t mzo)
{
	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

	void *ptr;
	if (malloc_check_start) {
		internal_check();
	}
	ptr = zone->calloc(zone, num_items, size);

	if (os_unlikely(malloc_logger)) {
		malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
				(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
	}

	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
	if (os_unlikely(ptr == NULL)) {
		malloc_set_errno_fast(mzo, ENOMEM);
	}
	return ptr;
}

方法return ptr,而 ptr = zone->calloc(zone, num_items, size),所以,po一下zone->calloc

(lldb) po zone->calloc
(.dylib`default_zone_calloc at malloc.c:385)

直接定位到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);
}

再次来到zone->calloc,打印一下

(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $1 = 0x00000001083722f9 (.dylib`nano_calloc at nano_malloc.c:878)
(lldb) 

可以看到nano_malloc,定位到nano_malloc方法

static void *
nano_malloc(nanozone_t *nanozone, size_t size)
{
	if (size <= NANO_MAX_SIZE) {
		void *p = _nano_malloc_check_clear(nanozone, size, 0);
		if (p) {
			return p;
		} else {
			/* FALLTHROUGH to helper zone */
		}
	}

	malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
	return zone->malloc(zone, size);
}

定位关键点是return p,上次的_nano_malloc_check_clear方法是重点,点击进去

static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
	MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);

	void *ptr;
	size_t slot_key;
	size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
	mag_index_t mag_index = nano_mag_index(nanozone);

	nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);

	ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
	if (ptr) { … }
#endif /* DEBUG */
#endif /* NANO_FREE_DEQUEUE_DILIGENCE */

		((chained_block_t)ptr)->double_free_guard = 0;
		((chained_block_t)ptr)->next = NULL; // clear out next pointer to protect free list
	} else {
		ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
	}

	if (cleared_requested && ptr) {
		memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
	}
	return ptr;
}

return ptr,关键代码是ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index),最关键分配的大小字段是slot_bytes,找到size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); 点击segregated_size_to_fit进去

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;
}

可以看到k = (size + NANO_REGIME_QUANTA_SIZE - 1)>> SHIFT_NANO_QUANTUM ;slot_bytes = k << SHIFT_NANO_QUANTUM; NANO_REGIME_QUANTA_SIZE 的定义:#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 1616,所以就是对对象进行16字节对齐,这既是为何上面40在malloc会变成48

总结:堆开辟内存是16字节对齐, 结构体内部成员变量是8字节对齐,对象是16字节对齐