IOS 对象内存的开辟
IOS对象的创建中分析到了对象的创建在alloc函数,而实现alloc的主要有三个步骤
而对象内存的开辟主要是在前两步, 基本类型的字节长度
struct LGStruct1 {
int c; // 4 首先c 占用0,1,2,3
double a; // 8 根据内存对齐原则1,a必须要8的倍数位起始,所以不能从4开始,需要扩充都8,也就是占8~15 位
char b; // 1 b占用16位,满足对齐原则1
short d; // 2 d从17位开始的话不满足原则1,所以扩充到从18位开始,占用 18,19位,也就是占用了0~19 ,总共20个字节,但是根据原则3,总的占用字节数必须是结构体成员的最大子成员的整数倍,本结构体的最大子成员是doube,也就是a,占用8个字节,所以,结构体总的大小必须是8的整数倍,所以扩充到24.
}struct1; //结构体占用的内存大小是24个字节。
struct LGStruct2 {
double e; //8 e占用0~7个字节。
char f; //1 f 占用第8位字节,满足内存对齐原则
struct LGStruct1 stu; //stu从第9位字节开始,但是根据原则2,需要从内部最大子成员的整数倍开始,stu内存最大的子成员是a,占8个字节,也就是说stu要从8的整数倍开始,也就是从16位开始,stu.c占用16,17,18,19,stu.a根据原则1,要从8的倍数开始,所以在扩充到23,从24位开始,占用24~31,stu.c占用32位,满足对齐原则,stu.d 需用从2的倍数位开始,所以扩充内存到33位,从34位开始占用34,35
char g; //1 g从36位开始,占用36位
short h; //2 根据原则1,要从38位开始,占用38,39,所以结构体内部总共的元素占用是0~39,总共40个字节,根据原则3,LGStruct1和LGStruct2中内部元素中最大的子成员是double,占用8个字节,而计算的成员占用字节刚好是40,是8的倍数,所以满足原则3。
}struct2; //所以struct2总共占用字节数40个字节
我们可以运行代码,用sizeof关键字计算一下两个结构体的大小,验证一下。
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// 内存对齐
// 对象的内存对齐 - 来自于结构体
NSLog(@"%lu-%lu",sizeof(struct1),sizeof(struct2));
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
2020-09-20 23:44:48.288395+0800 001-内存对齐原则[51021:874042] 24-40
为什么要进行内存对齐
我们知道处理器然后为了提高读取效率,是以固定大小读取内存和寻址,假如对象内存不是对齐的,处理器读取的时候会出现什么问题,我们以结构体为例,来看一下
struct LGStruct2 {
char f;
double a;
}struct1;
如上面的结构体,假设下面的地址是从0x0开始的两个内块,LGStruct2对象存储从0x00开始,如果LGStruct2没有进行内存对齐的话,我们会发现LGStruct2中的成员a的地址是0x01-0x08,占据了两个内存块,如果处理器需要读取a的值,需要读取内存块1和内存块2,然后合并内存然后才能读取出a的值,但是如果LGStruct2进行了内存对齐,a的存储地址就是0x08~0x0f,刚好占据了一个内存块,处理器只需要读取一次内存就可以读取到a的值。
0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 //寻址内存块1
0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f //寻址内存块2
calloc 开辟对象内存
我们在IOS对象的创建知道calloc才是真正开辟内存空间的地方,instanceSize只是计算的内存的大小,而查看calloc源码需要在libmaloc的源码中查看。
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;
}
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();
}
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;
}
但是当我们的源码调试到
ptr = zone->calloc(zone, num_items, size)
的时候,发现源码无法继续了,这时候,我们可以试着打印一下zone->calloc,会发现,zone->clloc是一个函数指针,实际上指向了default_zone_calloc函数
(lldb) po zone->calloc
(.dylib`default_zone_calloc at malloc.c:331)
找到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_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;
}
_nano_malloc_check_clear函数里面有一个重要的方法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
}
//左移4位,右移4位,和之前的&~15相当,抹除了小于16的值为0
// abcd efgh >>4 = 0000 abcd
// 0000 abcd <<4 = abcd 0000
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;
}
由此可知,clloc在开辟内存的时候对开辟的内存大小也进行了16字节的内存对齐。我们可以验证一下,验证之前我们先要了解三个函数
-
sizeof:
运算符,计算传出的数据类型所占用内存的大小,编译时完成运算。 -
class_getInstanceSize:
获取实例对象所占用的内存大小,采用8字节对齐 -
malloc_size:
获取实例对象实际开辟的内存大小。
我们分别用三个函数计算一下对象占用的内存大小
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) char height;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *person = [LGPerson alloc];
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size((__bridge const void *)(person)));
}
return 0;
}
<LGPerson: 0x10041f210> - 8 - 40 - 48
sizeof
计算出的大小是8,因为person是一个指针,所以大小是8;
class_getInstanceSize
计算的大小是40,是因为计算出来的大小是8字节对齐的;
malloc_size
计算的是person实际开辟内存的大小,48是16的倍数,刚好和我们之前分析的alloc流程里面16字节对齐算法对应上,说明OC的对象内存开辟是以16字节对齐的。
总结
- OC最新的版本中对象开辟内存是以16字节对齐的,开辟内存的时候,instanceSize和calloc分别做了对内存做了两次矫正,保证了内存的16字节对齐。