目录

1.iOS底层二刷第一课alloc的流程剖析

1.为什么要内存对齐

cpu在拜访内存时,并不是逐个字节拜访的,而是以字长(word size)为单位拜访。

比如在iOS中32位体系,字长为4字节,也便是每次CPU拜访内存以4字节为一个单位长度。

这么规划的意图,是为了削减CPU拜访内存的次数,加大CPU拜访内存的吞吐量。比如相同读取8个字节的数据,一次读4个字节只需要读2次。

相同也是因为内存条实践上是切片的这种规划,如下图:

iOS底层二刷之二结构体内存对齐

实践的:

iOS底层二刷之二结构体内存对齐

cpu取数据是按块去读的,假如一位一位的读那么功率会降低十分多;

那么按块读的话,假如不进行内存对齐,当取两个,四个或许八个字节的数据就有或许跨chip。这样cpu就要经过两次寻址找到完好数据,并对数据进行拼接,功率上就损失了许多。因而以空间来置换时刻,会进行对齐。

2.iOS中的内存对齐字长

咱们在上一篇文章中关于alloc的剖析中遇到过对齐的问题。 源码是这样的:

#ifdef __LP64__ 
#define WORD_SHIFT 3UL 
#define WORD_MASK 7UL 
#define WORD_BITS 64 
#else 
#define WORD_SHIFT 2UL 
#define WORD_MASK 3UL 
#define WORD_BITS 32 
#endif
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize()); 
} 
static inline uint32_t word_align(uint32_t x) {  
    return (x + WORD_MASK) & ~WORD_MASK; 
} 
  • 在iOS中64位体系下,是按照8字节对齐的
  • 在iOS中非64位体系下,是按照4字节对齐的

3.目标影响内存巨细的要素有什么

首要来认识一下下面的几个内存相关的办法,便利咱们剖析内存巨细的影响要素。
  • sizeof

sizeofC++中的运算符,作用是回来变量、目标、以及数据类型所占内存的字节数,它回来的巨细和体系相关。作用于根本类型,回来根本类型变量的字节巨细;作用于自定义类型,回来自定义类型及变量的巨细。

  • class_getInstanceSize

Returns the size of instances of a class

这个办法是由runtime提供的获取类的实例所占用的内存巨细。

  • malloc_size

/* Returns size of given ptr */

回来传入的指针所指向的内存空间的巨细。

搞清楚上面的这几个办法,咱们分别来经过添加,特点、办法、协议、还有分类,经过打印来调查下对拓荒的内存的影响。

  • 特点
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
LGPerson *person = [LGPerson alloc];
person.name   = @"NiuNiu";
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size(( __bridge const void )(person)));

打印成果:

体系内存拓荒剖析[56214:11947755] <LGPerson: 0x600000014000> – 8 – 16 – 16

依据打印成果:能够看到

  1. 一个目标类型所占的字节数是8个;
  2. 实例person占用的内存空间巨细是16个字节;
  3. person指向的内存空间巨细是16个字节;

咱们给类LGPerson添加一个特点,然后看看内存的改变,添加了一个

@property (nonatomic, copy) NSString *nickName;

体系内存拓荒剖析[56367:11960749] <LGPerson: 0x600000205380> – 8 – 24 – 32

发生了改变

占用的内存巨细添加了8个,变成了24;person指向的内存空间添加了16变成了32;

持续,咱们添加特点

@property (nonatomic, assign) int age;

此时的打印:

体系内存拓荒剖析[56437:11966024] <LGPerson: 0x600000201ba0> – 8 – 32 – 32

占用的内存巨细添加了8个,变成了32;person指向的内存空间和上一次比没变还是32;

先不说这个内存拓荒的规矩是什么,后边会说这个对齐的规矩和原因。咱们能够经过这个打印看到,特点是对拓荒内存的巨细有影响的

  • 办法 咱们持续在类LGPerson中添加办法来看看办法对内存的影响,添加了办法
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
- (void)run;
@end
@implementation LGPerson
- (void)run{
  NSLog(@"niuniu go run!");
}
@end

成果:

体系内存拓荒剖析[56565:11976327] <LGPerson: 0x6000002055e0> – 8 – 32 – 32

并没有发生改变,我再依次添加了几个办法,或许是类办法,这个打印的巨细都不会发生改变。

  • 协议

iOS底层二刷之二结构体内存对齐
也是没有改变的。

  • 分类

iOS底层二刷之二结构体内存对齐

嗯,也没什么改变。

结论

办法、协议、还有分类这几个对目标内存的拓荒无影响。

4.内存对齐的规矩

  • 数据成员对齐规矩:结构或联合的数据成员,第一个数据成员放在offset为0的当地,今后每个数据成员存储的起始方位要从该成员巨细或许子成员巨细的整数倍开端。

    • 例如:int是四个字节,那么要从4的整数倍地址开端存储。
  • 结构体作为成员,假如一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素巨细的整数倍地址开端存储。

    • 例如:struct a里有struct b,b里有char,int,double等元素,那么b要从8的倍数的方位开端存。
  • 结构体总巨细,也便是sizeof的成果,有必要是其内部最大成员的整数倍。缺乏要补齐。

根本数据类型占内存巨细

iOS底层二刷之二结构体内存对齐

  • 实战演练
struct Struct1 {
    double a;   // [0,7]
    char b;     // [8]
    int c;      // 依据规矩一要从4的倍数开端,所以[12,13,14,15]。跳过9,10,11
    short d;    //[16,17]
}struct1;
//依据第三准则总巨细要是8的倍数,那就要分配24字节。
struct Struct2 {
    double a;     //[0,7] 
    int b;        //[8,11]
    char c;       //[12]
    short d;      //依据规矩一跳过13,从14开端 [14,15]
}struct2;
//这儿0~15巨细原本就为16了,所以不需要补齐了。

5.相关源码探究

咱们在上一节课剖析alloc的流程里,实践拓荒内存空间的代码是:

iOS底层二刷之二结构体内存对齐

是办法calloc办法,它在objc中的只有一个声明,没有详细的完成

void *calloc(size_t __count, size_t __size) __result_use_check __alloc_size(1,2);

它的完成在libmalloc中。持续检查源码来看详细的完成。

  • 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)
{	
	void *ptr;
	if (malloc_check_start) {
		internal_check();
	}
	// Cooci 和谐学习不急不躁 这个流程是有优化的 会和苹果原体系不一定完全重合
	ptr = zone->calloc(zone, num_items, size);
	if (os_unlikely(malloc_instrumented || malloc_check_start ||
				malloc_logger || zone->version < 13)) {
		return _malloc_zone_calloc_instrumented_or_legacy(zone, num_items, size, mzo);
	}
	return zone->calloc(zone, num_items, size);
}

这儿中心的调用是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 */

发现没有下面的完成了,咱们经过po/p的方式来打印下实践的下一步函数的指向,(我了解的是实践存储的其实便是下一步要跳转到的办法地址)。 来到了(.dylib'default_zone_calloc at malloc.c:504),也便是办法default_zone_calloc

iOS底层二刷之二结构体内存对齐

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

iOS底层二刷之二结构体内存对齐
咱们经过相同的方式,找到了default_zone_calloc中的zone->calloc指向,(.dylib'szone_calloc at magazine_malloc.c:322),找到了szone_calloc,持续跟进,找到办法

void *
szone_calloc(szone_t *szone, size_t num_items, size_t size)
{
    size_t total_bytes;
    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
            return NULL;
    }
    return szone_malloc_should_clear(szone, total_bytes, 1);
}

找到中心办法szone_malloc_should_clear,进一步检查

MALLOC_NOINLINE void *
szone_malloc_should_clear(szone_t *szone, size_t size, boolean_t cleared_requested)
{
	void *ptr;
	msize_t msize;
	if (size <= TINY_LIMIT_THRESHOLD) {
		// size + 15 >> 4 << 4 (size 16字节对齐)
		// (size + (1 << 4) - 1) >> 4 ?????? = (size + 16-1)>> 4
		// #define SHIFT_TINY_QUANTUM 4ull
		msize = TINY_MSIZE_FOR_BYTES(size + TINY_QUANTUM - 1);
		if (!msize) {
			msize = 1;
		}
		// MALLOC_TRACE(TRACE_tiny_malloc, (uintptr_t)rack, TINY_BYTES_FOR_MSIZE(msize), (uintptr_t)tiny_mag_ptr, cleared_requested);
		// #define TINY_BYTES_FOR_MSIZE(_m) ((_m) << SHIFT_TINY_QUANTUM)
		// (size + (1 << 4) - 1) >> 4 ?????? = (size + 16-1)>> 4 << 4
		ptr = tiny_malloc_should_clear(&szone->tiny_rack, msize, cleared_requested);
	} else if (size <= SMALL_LIMIT_THRESHOLD) {
		//(size + 1<<9 -1 )>>9<<9
		msize = SMALL_MSIZE_FOR_BYTES(size + SMALL_QUANTUM - 1);
		if (!msize) {
			msize = 1;
		}
		ptr = small_malloc_should_clear(&szone->small_rack, msize, cleared_requested);
#if CONFIG_MEDIUM_ALLOCATOR
	} else if (szone->is_medium_engaged && size <= MEDIUM_LIMIT_THRESHOLD) {
		msize = MEDIUM_MSIZE_FOR_BYTES(size + MEDIUM_QUANTUM - 1);
		if (!msize) {
			msize = 1;
		}
		ptr = medium_malloc_should_clear(&szone->medium_rack, msize, cleared_requested);
#endif
	} else {
		size_t num_kernel_pages = round_large_page_quanta(size) >> large_vm_page_quanta_shift;
		if (num_kernel_pages == 0) { /* Overflowed */
			ptr = 0;
		} else {
			ptr = large_malloc(szone, num_kernel_pages, 0, cleared_requested);
		}
	}
#if DEBUG_MALLOC
	if (LOG(szone, ptr)) {
		malloc_report(ASL_LEVEL_INFO, "szone_malloc returned %p\n", ptr);
	}
#endif
	/*
	 * If requested, scribble on allocated memory.
	 */
	if ((szone->debug_flags & MALLOC_DO_SCRIBBLE) && ptr && !cleared_requested && size) {
		memset(ptr, SCRIBBLE_BYTE, szone_size(szone, ptr));
	}
	if (os_unlikely(!ptr)) {
		malloc_set_errno_fast(MZ_POSIX, ENOMEM);
	}
	return ptr;
}

这个分为三种size:tinysmalllarge,咱们以small为例进行详细的剖析。

if (size <= SMALL_LIMIT_THRESHOLD) {
	// (size + (1<<9) -1 )>>9
        msize = SMALL_MSIZE_FOR_BYTES(size + SMALL_QUANTUM - 1);
        if (!msize) {
                msize = 1;
        }
        // (size +  511 )>>9 << 9
        ptr = small_malloc_should_clear(&szone->small_rack, msize, cleared_requested);
}

前面的SMALL_MSIZE_FOR_BYTES是对齐的一个操作,是512,重点来看办法small_malloc_should_clear

small_malloc_should_clear

加了一小部分的注释,这个办法我也只看懂了一小部分,了解了大约

void *
small_malloc_should_clear(rack_t *rack, msize_t msize, boolean_t cleared_requested)
{
	void *ptr;
	//获取 magazine_t
	mag_index_t mag_index = small_mag_get_thread_index() % rack->num_magazines;
	magazine_t *small_mag_ptr = &(rack->magazines[mag_index]);
	MALLOC_TRACE(TRACE_small_malloc, (uintptr_t)rack, SMALL_BYTES_FOR_MSIZE(msize), (uintptr_t)small_mag_ptr, cleared_requested);
	//加锁
	SZONE_MAGAZINE_PTR_LOCK(small_mag_ptr);
	//假如配置了缓存
#if CONFIG_SMALL_CACHE
	//取上一次释放的空间
	ptr = small_mag_ptr->mag_last_free;
	// 假如上一次刚释放出来的空间 和 传进来的巨细正好相同 直接回来
	if (small_mag_ptr->mag_last_free_msize == msize) {
		// we have a winner 很强 很走运 
		small_mag_ptr->mag_last_free = NULL;
		small_mag_ptr->mag_last_free_msize = 0;
		small_mag_ptr->mag_last_free_rgn = NULL;
		SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
		CHECK(szone, __PRETTY_FUNCTION__);
		if (cleared_requested) {
			memset(ptr, 0, SMALL_BYTES_FOR_MSIZE(msize));
		}
		return ptr;
	}
#endif /* CONFIG_SMALL_CACHE */
	//没开启缓存,直接从small_malloc_from_free_list中读
	while (1) {
		//会匹配到一块和msize巨细相同的空间的地址回来
		ptr = small_malloc_from_free_list(rack, small_mag_ptr, mag_index, msize);
		if (ptr) {
			SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
			CHECK(szone, __PRETTY_FUNCTION__);
			if (cleared_requested) {
				memset(ptr, 0, SMALL_BYTES_FOR_MSIZE(msize));
			}
			return ptr;
		}
#if CONFIG_RECIRC_DEPOT
		//支撑存储,再试一次从small_malloc_from_free_list中读
		if (small_get_region_from_depot(rack, small_mag_ptr, mag_index, msize)) {
			ptr = small_malloc_from_free_list(rack, small_mag_ptr, mag_index, msize);
			if (ptr) {
				SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
				CHECK(szone, __PRETTY_FUNCTION__);
				if (cleared_requested) {
					memset(ptr, 0, SMALL_BYTES_FOR_MSIZE(msize));
				}
				return ptr;
			}
		}
#endif // CONFIG_RECIRC_DEPOT
		// The magazine is exhausted. A new region (heap) must be allocated to satisfy this call to malloc().
		// The allocation, an mmap() system call, will be performed outside the magazine spin locks by the first
		// thread that suffers the exhaustion. That thread sets "alloc_underway" and enters a critical section.
		// Threads arriving here later are excluded from the critical section, yield the CPU, and then retry the
		// allocation. After some time the magazine is resupplied, the original thread leaves with its allocation,
		// and retry-ing threads succeed in the code just above.
		//以上操作都没有读到适宜的内存空间,那么需要额外拓荒新的heap去满足这次拓荒的请求
		if (!small_mag_ptr->alloc_underway) {
			void *fresh_region;
			// time to create a new region (do this outside the magazine lock)
			small_mag_ptr->alloc_underway = TRUE;
			OSMemoryBarrier();
			SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
			//请求新的page
			fresh_region = mvm_allocate_pages(SMALL_REGION_SIZE,
					SMALL_BLOCKS_ALIGN,
					MALLOC_FIX_GUARD_PAGE_FLAGS(rack->debug_flags),
					VM_MEMORY_MALLOC_SMALL);
			SZONE_MAGAZINE_PTR_LOCK(small_mag_ptr);
			// DTrace USDT Probe
			MAGMALLOC_ALLOCREGION(SMALL_SZONE_FROM_RACK(rack), (int)mag_index, fresh_region, SMALL_REGION_SIZE);
			//内存溢出了 回来空
			if (!fresh_region) { // out of memory!
				small_mag_ptr->alloc_underway = FALSE;
				OSMemoryBarrier();
				SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
				return NULL;
			}
			region_set_cookie(&REGION_COOKIE_FOR_SMALL_REGION(fresh_region));
			//新请求的page经过hash刺进到固定的方位
			// small_malloc_from_region_no_lock 内部包含刺进rack_region_insert
			// magazine_t的结构是一个双向链表 ,能够进一步检查下他的数据结构
			//small_malloc_from_region_no_lock 办法中也有对新拓荒的空间的双向链表的刺进处理 recirc_list_splice_last
			ptr = small_malloc_from_region_no_lock(rack, small_mag_ptr, mag_index, msize, fresh_region);
			// we don't clear because this freshly allocated space is pristine
			small_mag_ptr->alloc_underway = FALSE;
			OSMemoryBarrier();
			SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
			CHECK(szone, __PRETTY_FUNCTION__);
			return ptr;
		} else {
			SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
			yield();
			SZONE_MAGAZINE_PTR_LOCK(small_mag_ptr);
		}
	}
	/* NOTREACHED */
}
  • 首要获取了一个 magazine_t,它本身是一个双向链表的结构。

  • 假如支撑了缓存战略,会先从它自己的mag_last_free中,读到上一次刚被释放出来的地址,判别和现在请求的msize是不是相同,假如刚好相同,那么!很好,很走运,直接把这个地址回来。

  • 假如没有那么走运,那么按部就班让small_malloc_from_free_list处理,也便是从magazine_t->mag_free_list中进行读取闲暇的空间,也便是free_list

  • 假如支撑循环存储,再试一次从small_malloc_from_free_list中读取一遍。

  • 假如上面一系列操作猛如虎,也都没有找到适宜的空间地址分配给msize,那么拓荒新的page。

    • 拓荒新的page,首要加锁。
    • 判别是否溢出,溢出回来null
    • 没有溢出,拓荒新的page成功的话,进行刺进操作small_malloc_from_region_no_lock
    • 办法small_malloc_from_region_no_lock内部经过rack_region_insert办法,进行应该是hash计算找到适宜的方位进行刺进。
    • 并经过recirc_list_splice_last进行新的空间刺进到page中,把双向链表的指针指向进行处理。
  • ptr回来给msize,从此msize有了名字,便是ptr的地址~

总结

malloc的进程还是十分严谨的,之所以有这样的进程,依赖于本身的数据结构规划,像magazine_t这种,本身是一个双向链表,内部还存储了自己的free_list,一个magazine_t的结构中带了自己的许多存储信息,拓荒信息,上一次释放内容的这样的信息等。

这次对内存对齐的了解比第一次学习的也更加深入了一些,希望再看第三遍,第四遍或许第五遍的时分,能了解的更好。

附上上一年写的博客,有个深入的对照:www.jianshu.com/p/700833c11…

参考文章:

  • linux内核:内存分配alloc_pages 快速途径代码剖析
  • libmalloc初探——TinyHeap
  • iOS 高级之美(六)—— malloc剖析
  • 百度APP iOS端内存优化-原理篇