OC底层探究之内存对齐

847 阅读10分钟

有兴趣的朋友可以观看我的上一篇文章《OC对象原理探究之alloc探索》

一、对象的内存空间

先给HPerson添加对象:

ia_400000041.png

打上断点,运行,然后在lldb中输入x p,显示对象p的内存分布:

ia_400000042.png

0x108f079c0是对象p的内存首地址,接下来就是对象p的内存。

iOS端为小端模式,所有需要倒着读取:

ia_400000043.png

内存打印出来是什么呢?

isa

为什么没有打印出isa呢?

因为要&ISA_MASK:

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#   endif

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL

# else
#   error unknown architecture for packed isa
# endif

这里使用的是真机,所以&0x0000000ffffffff8:

ia_400000044.png

现在就正确的打印出isa了!

后面的0则为对象的属性的存储空间!

进入debug查看一下内存:

ia_400000045.png

输入内存首地址:

ia_400000046.png

发现即使没有给属性赋值,依旧会开辟内存!

给属性赋值后再运行:

ia_400000047.png

x/5gx为格式化输出!

x/nuf <addr>
n表示要显示的内存单元的个数
—————
u表示一个地址单元的长度:
b表示单字节
h表示双字节
w表示四字节
g表示八字节
—————
f表示显示方式,可取如下值:
x按十六进制格式显示变量
d按十进制格式显示变量
u按十进制格式显示无符号整型
o按八进制格式显示变量
t按二进制格式显示变量
a按十六进制格式显示变量
i指令地址格式
c按字符格式显示变量
f按浮点数格式显示变量

如果把height改为BOOL类型:

ia_400000048.png

就会发现ageheight放在了一起!

这是苹果的底层对内存进行了优化,即内存对齐

二、对象的内存影响因素

先引入runtime

#import <objc/runtime.h>

然后打印对象实例大小

发现大小为32

我们再删除一些属性,重新打印:

发现大小为16

说明属性对类的大小是有影响的!

加上成员变量后再打印:

发现大小为24

说明成员变量对类的大小也是有影响的!

那么方法是否会影响类的大小呢?

添加方法

发现类的大小依旧是24

说明方法对类的大小是没有影响的!

结论:只有成员变量会影响类的大小!

如下图所示:

三、结构体内存对齐

1、各类型所用字节:

2、内存对⻬的原则:

1:数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要
从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,
结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存
储。

min(当前开始的位置m n) m = 9 n = 4
 9 10 11 12
 
2:结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从
其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b
⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)

3:收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤
成员的整数倍.不⾜的要补⻬。

3、分析结构体:

先看看这2个结构体

结构体1:

struct LGStruct1 {
    double a;      
    char b;        
    int c;         
    short d;       
}struct1;

结构体2:

struct LGStruct2 {
    double a;     
    int b;        
    char c;       
    short d;      
}struct2;

这2个结构体占用的字节是否一样呢?

我们来分析一下:

结构体1:

double为8字节,从8的整数倍开始,占用8字节,所以a[0-7]

char为1字节,从1的整数倍开始,占用1字节,所以b[8]

int为4字节,从4的整数倍开始,占用4字节,所以91011空出,c在[12-15]

short为2字节,从2的整数倍开始,占用2字节,所以d在[16-17]

所用结构体1一共18字节。

但是根据内存对齐原则,结构体的总⼤⼩为内部最⼤成员的整数倍,即24

结构体2:

double为8字节,从8的整数倍开始,占用8字节,所以a[0-7]

int为4字节,从4的整数倍开始,占用4字节,所以b[8-11]

char为1字节,从1的整数倍开始,占用1字节,所以c在[12]

short为2字节,从2的整数倍开始,占用2字节,所以13空出,d在[14-15]

所用结构体2一共16字节。

根据内存对齐原则,总大小为16

再来看看结构体3:

struct LGStruct3 {
    double a;
    int b;
    char c;
    short d;
    int e;
    struct LGStruct1 str;
}struct3;

这种结构体嵌套的结构体又该怎么分析呢?

double为8字节,从8的整数倍开始,占用8字节,所以a[0-7]

int为4字节,从4的整数倍开始,占用4字节,所以b[8-11]

char为1字节,从1的整数倍开始,占用1字节,所以c在[12]

short为2字节,从2的整数倍开始,占用2字节,所以13空出,d在[14-15]

int为4字节,从4的整数倍开始,占用4字节,所以e在[16-19]

LGStruct1为24字节,LGStruct1内最大成员为8字节,所以从8的整数倍开始,占24字节,所以str在[24-47]

所用结构体3一共48字节。

根据内存对齐原则,总大小为48

我们看看打印的结果吧:

和我们分析的一致!

那么我们平时写属性的时候需要注意顺序吗?

来看看对象内存分布就知道了:

发现内存分布并没有按照属性顺序排序!

xcode已经帮我们做了优化了!

也可以进行二进制类型的重排数据类型的重排等等。

4、为什么需要内存对齐?

一个结构体有char、int、long类型:

如果没有内存对齐:

处理器读取数据需要先读1字节,再读4字节,再读8自己,多次读取,效率低下!

如果有内存对齐:

处理器读取数据只需要8字节、8字节的去读就可以了!方便读取!效率非常高!

所以内存对齐就是空间换时间!提高效率!

四、malloc源码引入

依旧是Hperson

引入malloc.h

#import <malloc/malloc.h>

看看这个打印结果:

3个值分别是什么呢?

sizeof 是否是 8 + 8 + 4 + 8 = 28 ?还是 32 ?又或者是40呢?

class_getInstanceSize 呢?

malloc_size 呢?

来看看打印结果

分别为8、40、48

为什么呢?

p为指针,指针的大小为8!
class_getInstanceSize([HPerson class])获取的是HPerson类的大小!
HPerson类有isa、name、nickName、age、height,共5个成员变量!所以大小为40

那么malloc_size又是什么呢?

我们点进去看看:

发现没有实现

再看看路径

苹果的源代码目录下载源码:

然后可以参考《libmalloc源码浅谈》进行编译!

五、malloc分析探索思路

在上一篇文章《OC对象原理探究之alloc探索》中我们已经知道了calloc是开辟内存的方法。

现在我们就来探索一下calloc

先在源码的main.h中使用calloc方法:

跟进去看看:

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

再进入到_malloc_zone_calloc方法:

MALLOC_NOINLINE
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,再根据return寻找:

跟着calloc进去:

发现只是一个声明

接下来怎么探索呢?

全局搜索calloc

发现有赋值,即有存储值,那么就可以打印

断上断点进行打印

全局搜索找到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进去:

static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
	size_t total_bytes;

	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 */
		}
	}
	malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
	return zone->calloc(zone, 1, total_bytes);
}

那么,nano_calloc方法的重点又在哪呢?

先看return

第一个return NULL;返回,这种肯定有问题,可以忽略!

那么return p;return zone->calloc(zone, 1, total_bytes);会走哪个呢?

return p;前有个if (total_bytes <= NANO_MAX_SIZE)可得出,正常情况肯定是走return p;

然后进入创建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) {
		unsigned debug_flags = nanozone->debug_flags;
#if NANO_FREE_DEQUEUE_DILIGENCE
		size_t gotSize;
		nano_blk_addr_t p; // the compiler holds this in a register

		p.addr = (uint64_t)ptr; // Begin the dissection of ptr
		if (NANOZONE_SIGNATURE != p.fields.nano_signature) {
			malloc_zone_error(debug_flags, true,
					"Invalid signature for pointer %p dequeued from free list\n",
					ptr);
		}

		if (mag_index != p.fields.nano_mag_index) {
			malloc_zone_error(debug_flags, true,
					"Mismatched magazine for pointer %p dequeued from free list\n",
					ptr);
		}

		gotSize = _nano_vet_and_size_of_free(nanozone, ptr);
		if (0 == gotSize) {
			malloc_zone_error(debug_flags, true,
					"Invalid pointer %p dequeued from free list\n", ptr);
		}
		if (gotSize != slot_bytes) {
			malloc_zone_error(debug_flags, true,
					"Mismatched size for pointer %p dequeued from free list\n",
					ptr);
		}

		if (!_nano_block_has_canary_value(nanozone, ptr)) {
			malloc_zone_error(debug_flags, true,
					"Heap corruption detected, free list canary is damaged for %p\n"
					"*** Incorrect guard value: %lu\n", ptr,
					((chained_block_t)ptr)->double_free_guard);
		}

#if defined(DEBUG)
		void *next = (void *)(((chained_block_t)ptr)->next);
		if (next) {
			p.addr = (uint64_t)next; // Begin the dissection of next
			if (NANOZONE_SIGNATURE != p.fields.nano_signature) {
				malloc_zone_error(debug_flags, true,
						"Invalid next signature for pointer %p dequeued from free "
						"list, next = %p\n", ptr, "next");
			}

			if (mag_index != p.fields.nano_mag_index) {
				malloc_zone_error(debug_flags, true,
						"Mismatched next magazine for pointer %p dequeued from "
						"free list, next = %p\n", ptr, next);
			}

			gotSize = _nano_vet_and_size_of_free(nanozone, next);
			if (0 == gotSize) {
				malloc_zone_error(debug_flags, true,
						"Invalid next for pointer %p dequeued from free list, "
						"next = %p\n", ptr, next);
			}
			if (gotSize != slot_bytes) {
				malloc_zone_error(debug_flags, true,
						"Mismatched next size for pointer %p dequeued from free "
						"list, next = %p\n", ptr, next);
			}
		}
#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;
}

这种很长的代码应该怎么解读呢?

就从小玩到大!

if折叠起来看:

这就很清楚了:存在ptr就会怎么怎么样,不存在ptr又会怎么怎么样!

先看看存在ptr会做什么!

一看就发现if里面都是调用malloc_zone_error方法,很明显,这个方法是报错的方法!

再看看不存在ptr会做什么!

进入segregated_next_block方法:

static MALLOC_INLINE void *
segregated_next_block(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
{
	while (1) {
		uintptr_t theLimit = pMeta->slot_limit_addr; // Capture the slot limit that bounds slot_bump_addr right now
		uintptr_t b = OSAtomicAdd64Barrier(slot_bytes, (volatile int64_t *)&(pMeta->slot_bump_addr));
		b -= slot_bytes; // Atomic op returned addr of *next* free block. Subtract to get addr for *this* allocation.

		if (b < theLimit) {   // Did we stay within the bound of the present slot allocation?
			return (void *)b; // Yep, so the slot_bump_addr this thread incremented is good to go
		} else {
			if (pMeta->slot_exhausted) { // exhausted all the bands availble for this slot?
				pMeta->slot_bump_addr = theLimit;
				return 0;				 // We're toast
			} else {
				// One thread will grow the heap, others will see its been grown and retry allocation
				_malloc_lock_lock(&nanozone->band_resupply_lock[mag_index]);
				// re-check state now that we've taken the lock
				if (pMeta->slot_exhausted) {
					_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
					return 0; // Toast
				} else if (b < pMeta->slot_limit_addr) {
					_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
					continue; // ... the slot was successfully grown by first-taker (not us). Now try again.
				} else if (segregated_band_grow(nanozone, pMeta, slot_bytes, mag_index)) {
					_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
					continue; // ... the slot has been successfully grown by us. Now try again.
				} else {
					pMeta->slot_exhausted = TRUE;
					pMeta->slot_bump_addr = theLimit;
					_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
					return 0;
				}
			}
		}
	}
}

发现是一个死循环

里面有一个注释

// Did we stay within the bound of the present slot allocation?

即可知segregated_next_block方法是在寻找可用内存空间

但是我们的重点是在找这个对象的大小

那么是哪个决定了大小呢?

在上一个方法_nano_malloc_check_clear方法里面:

ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);

很明显slot_bytes就是大小!即:

size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here

于是我们进入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;
}

再看看NANO_REGIME_QUANTA_SIZE

#define NANO_MAX_SIZE			256 /* Buckets sized {16, 32, 48, ..., 256} */
#define SHIFT_NANO_QUANTUM		4
#define NANO_REGIME_QUANTA_SIZE	(1 << SHIFT_NANO_QUANTUM)	// 16
#define NANO_QUANTA_MASK		(NANO_REGIME_QUANTA_SIZE - 1)
#define NANO_SIZE_CLASSES		(NANO_MAX_SIZE/NANO_REGIME_QUANTA_SIZE)

得到了确定大小公式

k = size + 15

然后k先右移4位再左移4位

即向上取16的整数倍!

这就是内存对齐的公式!即16字节对齐

上一章字节对齐公式类似!

字节对齐即为向上取8的整数倍,也就是先右移3位左移3位

所以:

对象的内存对齐是16字节对齐!
成员变量的对齐是8字节对齐!

六、对象内存对齐原理

对象的内存对齐为什么是16字节呢?

比如有3个对象的大小都为8 * 3 = 24字节!

如果内存对齐是8字节的话,这3个对象就会紧密相连

容易造成访问错误

如果内存对齐是16字节的话,就会有空余提高容错率

同时,对象来自于NSObject,而NSObject自身有一个isa成员变量,所以再加任意一个成员变量的对象的大小至少都是16字节!

所以对象的内存对齐是按照16字节对齐!