iOS底层原理探索-02- 内存对齐原则&calloc

853 阅读6分钟

《目录-iOS & OpenGL & OpenGL ES & Metal》
今天接着上一篇继续探索 内存对齐原则 & calloc。

一、内存对齐

1、内存对齐原则

  1. 数据成员对齐规则:结构(struct)或者联合(union)的数据成员,第一个数据成员放在offset为0的地方,之后的每个数据成员存储的起始位置 要从该成员大小 或者该成员的子成员大小(只要该成员有子成员,比如:数组、结构体等) 的整数倍开始。(举例:int是4字节的,那么它要从4的整数倍地址开始存储)

  2. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要以 它内部最大元素的大小的整数倍地址开始存储。(举例:struct a 里面有一个 struct b,在b里面有char、int、double三个元素,char是1个字节,int是4个字节,double是8个字节。那么b就应该从8的整数倍地址开始存储)

  3. 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部大小最大成员的整数倍,不足的要补齐。

2、代码解释

先放一张参考值的图 数据类型所占字节数

上代码:

//char是1个字节,int是4个字节,short是2个字节,double是8个字节
struct YStruct1 {
    char a;     // char是1,从0开始存,接着存double,double是8 要从8开始,所以这里要补齐(1~7),最后是0~7
    double b;   // 从8开始,占8位,最后是8~15
    int c;      // 从16开始,刚好16是4的倍数,最后是16~19
    short d;    // 从20开始,刚好20是2的倍数,最后是20~21,但是要满足是最大成员的整数倍,补齐23、24,这里最后是20~24
} MyStruct1;

struct YStruct2 {
    double b;   // 0~7
    char a;     // 8
    int c;      // 9不是4的整数倍(8后面补齐9、10、11),从12开始,12~15
    short d;    // 16开始,16~17。最后一个是17,补齐8的倍数,最后是16~24
} MyStruct2;


struct YStruct3 {
    double b;   // 0~7
    int c;      // 8~11
    char a;     // 12
    short d;    // 13~14。补齐,最后是13~16
} MyStruct3;

//那我们看一下结构体里放结构体。4就是在3里面放个2

struct YStruct4 {
    double b;   // 0~7
    int c;      // 8~11
    char a;     // 12
    short d;    // 13~14
    struct YStruct2 e;//15开始,我们知道YStruct2里面最大元素是8,要从8的整数倍开始,就是要从16开始,(13~14)后面补齐(15),16+24.最后是16~40,刚好40又是8的整数
} MyStruct4;



打印:
NSLog(@"%lu-%lu-%lu-%lu",sizeof(MyStruct1),sizeof(MyStruct2),sizeof(MyStruct3),sizeof(MyStruct4));

结果:
24-24-16-40

这里面其实还涉及到一部分内存优化,好比如果每一个属性都是8字节对齐,针对YStruct3就应该是占用了32个字节,而内存对齐之后,只占用了16个字节,相对节省了内存空间

3、对象申请内存VS系统开辟内存

上篇文章的末尾提到过size方法。size_t size = cls->instanceSize(extraBytes);那我们创建一个对象,声明4个属性看一看,直接调用这个方法打印一下size:

//直接调用这个方法,并没有最小为16的判断,不影响后面的思路
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

Person.h 只声明

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, strong) NSString *hobby;

@end

ViewController.m

Person  *per = [Person alloc];
//isa 						//8
per.name = @"superMan"; 	//8
per.age  = 18;				//4 字节对齐 + 4
per.height = 185;			//8
per.hobby  = @"女";		   //8

//打印:
NSLog(@"%lu",class_getInstanceSize([per class]));

//结果:
40

明明只有4个属性,应该是 4 x 8 = 32啊!为什么是40呢?这就涉及到一个小知识点:对象的第一个属性是isa。所以是 5 x 8 = 40。

那么我再添加2个char属性,来看一下是否有上面提到的内存优化

//person.h 添加
@property (nonatomic) char ch1;
@property (nonatomic) char ch2;


//ViewController.m 
per.ch1    = 'a';
per.ch2    = 'b';

//打印:
NSLog(@"%lu",class_getInstanceSize([per class]));

//结果:
40

通过LLDB调试看一下内存段的值:

由上图可以看出,在内存里,系统帮我们做了内存优化,导致一些属性的位置发生了变化

那我们创建几个属性来看一下 对象申请内存 和 系统开辟内存 是否一样。直接调用malloc_size方法

//打印:
NSLog(@"%lu - %lu",class_getInstanceSize([per class]),malloc_size((__bridge const void *)(per)));

//结果:
40 - 48

奇怪了!

为什么结果不一样呢?我们就要来到 libmalloc 源码里看一下calloc方法实现 ↓、

二、calloc

我们配置好libmalloc 源码,直接执行下面代码进行探索:

//为什么传40? 
//是因为从上面得知,person类,在对象申请内存时候是40,探索怎么在calloc里面变成48

void *p = calloc(1, 40);
NSLog(@"%lu",malloc_size(p));

我们还一步步地源码以及流程

1、 calloc方法

void *
calloc(size_t num_items, size_t size)
{
	void *retval;
	retval = malloc_zone_calloc(default_zone, num_items, size);
	if (retval == NULL) {
		errno = ENOMEM;
	}
	return retval;
}

2、 malloc_zone_calloc方法

//这个方法好长啊,我们直接抓重点! 就是返回值的赋值
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

	void *ptr;
	if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
		internal_check();
	}
	//直接看这里!打断点,是走到这里了
    //但是有一个问题点,再进入calloc方法就会变成一个递归。
    //这里可以通过p来打印一下这个属性赋值方法指向哪里,就是在哪里实现的
	ptr = zone->calloc(zone, num_items, size);
	
	if (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);
	return ptr;
}

全局搜索这个方法,然后断点,走一下,果然进来了~

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

继续 p zone->calloc

4、 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;
	}
	//#define NANO_MAX_SIZE			256
	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);
}

5、_nano_malloc_check_clear方法

这一块代码很长,只看对我们有用的一块,我们的目的是看怎么拿到48的,找关键字byte

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;
    //很明显这里出现了byte
	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) {···//这里面是一些容错信息处理} 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;
}


6、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;
    /*
    查看系统宏定义
    #define SHIFT_NANO_QUANTUM		4
	#define NANO_REGIME_QUANTA_SIZE	(1 << SHIFT_NANO_QUANTUM)	// 1 左移4位 = 16
    */
	
	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 = 16
//SHIFT_NANO_QUANTUM = 4
//如果size = 0,那就赋值 16.也就是最小16
if (0 == size) {
    size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
// k = size + 16 - 1 右移4
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; 
// k 再左移4
slot_bytes = k << SHIFT_NANO_QUANTUM;	
    
    
/*
其实这个算法就相当于(我们传进来的size=40)

55 >> 4 << 4
55的二进制:  
0011 0111  
>>4
0000 0011
<<4
0011 0000

2的5次方+2的4次方 = 32 + 16 = 48!!!


至此,我们可以看出,右移4+左移4,就是16字节对齐~
我们再看上一篇的size算法,
(x + WORD_MASK) & ~WORD_MASK

(8+7)& ~7 = 8
我们算一下 15 右移3+左移3 是不是等于8
0000 1111
>>3
0000 0001
<<3
0000 1000
刚好就是8

相当于是右移3+左移3,对应的就是8字节对齐~
因为,2的4次方=16,2的3次方=8
*/

三、总结

  • 对象的属性是进行的 8 字节对齐(最小返回16字节)
  • 对象自己进行的是 16 字节对齐
    • 因为内存是连续的,通过 16 字节对齐规避风险和容错,防止访问溢出和野指针等问题
    • 同时,也提高了寻址访问效率,也就是空间换时间

对于上一篇来说,我们解决了 内存对齐原则 和 calloc的疑问, isa的探索放在下一篇~