1.回顾之前
前面我们讲过alloc的一些底层探索中,在分配内存的时候有涉及到内存对齐的概念。instanceSize()中alignedInstanceSize()内存分配粗略的讲到了内存对齐的概念,下面我们来详细了解一下
2.内存对齐-初探
什么是内存对齐?是否有很多问号,刚接触到这个概念的时候,也是很疑惑。概念:编译器在读取内存地址的时候,会按照一定的偏移量去读取;比如在一个写的struct里面定义一定变量,里面变量大小都是4字节,8字节,16 字节的,而且sizeof()大小也不是里面定义大小一样(大于里面定义的字节大小)。
为什么呢?下面👇我们来说明一下原因
内存对齐的原因
1.读取效率:cpu数据访问效率。一般平台系统会从基数位地址开始读取,那么需要两个周期才能拼凑成32bit/64bit。所以从效率上考虑系统会吧基数字节自动补齐成偶数位一个周期便可以读取完成,提高了读取效率
2.硬件原因:一部分平台不能访问任意地址上的数据的,需要访问特定指定类型的数据,不然会出现异常(具体哪些平台尚不了解,需从网上了解...)
内存对齐规则
1)数据成员对其规则:结构体(struct)或联合体(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数组、结构体等)的整数倍开始。
总结:int为4字节,则要从4的整数倍开始存储
2)结构体作为成员:如果一个结构体内部包含其他结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
总结:struct x中包含char ,int,double,float等元素,那应该从8(double字节大小)的整数倍开始存储
3)收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的需要补齐。在malloc中总是为16的倍数。
内存对齐-实战
举个栗子1
struct StructOne {
char a; // 1字节
double b; // 8字节
int c; // 4字节
short d; // 2字节
} Struct1;
struct StructTwo {
double b; // 8字节
int c; // 4字节
char a; // 1字节
short d; // 2字节
} Struct2;
struct StructOThree {
double b; // 8字节
char a; // 1字节
int c; // 4字节
short d; // 2字节
} Struct3;
NSLog(@"%lu---%lu---%lu",sizeof(Struct1),sizeof(Struct2),sizeof(Struct3));
输出结果为: 24—16—24
我们从内存对齐原则看,上面三个结构体在内存栈中的分布应该是这样的:
类对象内存开辟
1.准备工作
LLDB调试知识:
①: x/4gx 对象
表示输出4个16进制的8字节地址空间(x表示16进制,4表示4个,g表示8字节为单位,等同于x/4xg 对象
)
②:po
与p
:p表示"expression"——打印对象指针;而po是"expression -O"——打印对象本身
③:Xcode查看内存地址 debug->Debug Workflow->view memory
举个栗子🌰
@interface WXPerson : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) long height;
@property (nonatomic, assign) char c1;
@property (nonatomic, assign) float m_float;
@end
#import "WXPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
WXPerson *p = [[WXPerson alloc] init];
p.name = @"Kaemi"; // NSString 8
p.age = 18; // int 4
p.height = 188; // long 8
p.c1 = 'a'; // char 1
p.m_float = 12.6; // float 4
NSLog(@"sizeof:%lu---申请内存大小为:%lu——-系统开辟内存大小为:%lu",sizeof([p class]),class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
}
return 0;
}
输出结果为:sizeof:8---申请内存大小为:40——-系统开辟内存大小为:48
几个知识点:sizeof() class_getInstanceSize() malloc_size() 是什么
sizeof:它是一个运算符,在编译时就可以获取类型所占内存的大小
class_getInstanceSize:依赖于<objc/runtime.h>
,返回创建一个实例对象所需内存大小,不考虑malloc函数的话,内存对齐一般是以 8 字节对齐
malloc_size:依赖于<malloc/malloc.h>
,返回系统实际分配的内存大小,在Mac、iOS中的malloc函数分配的内存大小总是 16 的倍数
那为啥class_getInstanceSize返回的值是40,malloc_size返回时48呢?下面👇我们来分析一下
3.malloc源码分析
内存开辟,我们在
alloc
内存开辟中还遗留了一个问题:obj = (id)calloc(1, size)
,之前我们用objc源码无法断点下手,现在我们可以用libmalloc
源码来分析一下
在libmalloc源码
中新建target,按照objc源码
中的方式调用
void *p = calloc(1, 40);
断点malloc_zone_calloc();
通过调试台看一下具体实现
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x000000010031cd14 (.dylib`default_zone_calloc at malloc.c:249)
确定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);
}
继续用lldb查看:
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x000000010031e33f (.dylib`nano_calloc at nano_malloc.c:878)
再进去
最后我们会到这边
/*
* #define SHIFT_NANO_QUANTUM 4
* #define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
*/
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;
}
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;
}
class_getInstanceSize 是对象对8字节对齐,根据内存对齐原则可知分配了40大小;
malloc_size
中的48是怎么来的。这里有多个size_t类
,断点调试看了下的size
是我们传进来的40,而slot_bytes
刚好是我们的目标48,
在经过 (40 + 16 - 1) >> 4 << 4 操作后,结果为48,也就是16的整数倍——即16字节对齐
malloc_size
系统对对象做了16字节对齐
总结
对象的属性是8字节对齐
对象是16字节对齐
因为内存是连续的,通过 16 字节对齐规避风险和容错,防止访问溢出
同时,也提高了寻址访问效率,也就是空间换时间