OC对象(一)-- alloc和init底层到底在干嘛
OC对象(二)-- 内存对齐和calloc中的16字节对齐
OC对象(三)-- isa结构分析
内存对齐初探
实例对象在内存中的布局,是被系统优化过的,不会按照属性定义的顺序在内存中开辟空间。
举个例子:
定义一个Person类,里面包括一些属性
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) char ch1;
@property (nonatomic, assign) char ch2;
@end
@implementation Person
@end
初始化实例对象,给属性进行赋值:
Person *p = [Person alloc];
p.name = @"DragonetZ";
p.nick = @"DZ";
p.age = 18;
p.ch1 = 'a';
p.ch2 = 'z';
NSLog(@"p:%@", p);
使用lldb指令x/4gx p
查看实例对象的内存情况,p是实例对象的指针变量,打印结果如下:
(lldb) x/4gx p
0x600000683e20: 0x00000001061fb6c8 0x0000001200007a61
0x600000683e30: 0x00000001061f9018 0x00000001061f9038
- 第一个值0x00000001061fb6c8:实例对象的isa
- 第二个值0x0000001200007a61:里面存放着age、ch1、ch2的值,都是使用十六进制表示。0x12对应十进制18。0x7a对应的十进制122,ASCII中对应的就是‘z’,同理0x61对应的97,就是‘a’
- 第三个值0x00000001061f9018:就是name属性值‘DragonetZ’
- 第四个值0x00000001061f9038:就是nick属性值‘DZ’
如图中展示,内存中的排序和类型属性定义的顺序不一致。
拓展-lldb命令解释
上文用了lldb指令x/4gx p
,这里做一个简单解释:
- p:读取实例对象p的内存。也就是读取内存的起始位置。
- 第一个x:是
memory read
指令的简写,读取内存作用 - 4g:从起始位置开始,读取4段
- 第二个x:代表的是16机制的方式读取,同理可以切换成其他进制模式:‘o’代表八进制,‘t’代表二进制,‘d’代表十进制。
打印内容:
- 冒号左侧,也就是图中的红色框中代表的是内存地址,第一个地址也就是p的首地址。与
po p
的打印是相同过的。 - 冒号右侧,是地址中的值。
内存对齐原则
OC中,实例对象其实就是struct类型,因此我们研究一下struct是如何进行内存对齐的
简单的小案例
struct Struct1 {
double a;
char b;
int c;
short d;
}stu1;
struct Struct2 {
double a;
int b;
char c;
short d;
}stu2;
NSLog(@"%lu - %lu", sizeof(stu1), sizeof(stu2));
定义两个struct,每个struct中都有几个不同类型的成员,打印两个struct的内存占用情况。
stu1占用24个字节,stu2占用16字节。
此处可以使用下图来自己先计算一下:
原理知识点
- struct第一个数据成员,从偏移量offset的0位开始。后续的成员从自身的整数倍的偏移位置开始。
- 计算出来的总大小,需要是最大成员的整数倍。
用stu1解释说明:
struct Struct1 {
double a;
char b;
int c;
short d;
}stu1;
- double a是第一个成员,占用8个字节,根据说明,第一个成员offset是0,占用空间【0-7】
- char b,占用1个字节,offset是8,而且开始位置8是需要占用空间1的整数倍,所以可以存放【8】
- int c,占用4个字节,offset是9,开始位置9不是需要占用空间4的整数倍,需要后移到整数倍12上存放,占用的位置是【12-15】
- short d,占用2个字节,offset是16,16是2的整数倍,占用【16-17】
- 占用【0-17】共18个字节,成员中最大的是double,占8字节,所以取8的整数倍,就是24字节。
再来看看stu2:
struct Struct2 {
double a;
int b;
char c;
short d;
}stu2;
- double a,占用【0-7】
- int b,【8-11】,因为8是4的整数倍。
- char c,【12】
- shot d,【14-15】,因为开始位置13不是2的整数倍,因此从14开始
- 占用【0-15】,共16字节,最大成员double是8字节,取8整数倍,正好是16
扩展-struct嵌套
如果struct a中有另一个struct b作为它的成员,那么偏移量offset就取struct b中最大成员的整数倍开始
struct Struct2 {
double a;
int b;
char c;
short d;
}stu2;
struct Struct3 {
char a;
short b;
struct Struct2 c;
}stu3;
struct Struct2前面分析结构:占用16个字节,最大成员是8字节
分析stu3
- char a:【0】
- short b:【2-3】
- struct Struct2 c:找到8的整数倍作为开始位,【8-23】
- 取最大成员8的整数倍,也就是24
OC底层的优化
上面的例子中stu1和stu2两个struct可以说是差不多,但是一个占用24,一个占用16。但是我们在OC类的时候,属性顺序是不受影响的。说明苹果底层是对我们内存开辟进行优化过的。这里可以通过示例对象内存打印中可以发现,文章开始的例子中:
Person类中属性age、ch1、ch2的值都存放在内存中第二个位置(0x0000001200007a61)中。
calloc
之前文章alloc和init底层到底在干嘛!中分析了alloc流程,先是用16字节对齐的方式计算出size,然后调用calloc函数来开辟内存空间
//16字节对齐获取到size
size = cls->instanceSize(extraBytes);
//根据size开辟内存空间
obj = (id)calloc(1, size);
此时产生一个问题,如果传入的size没有进行16字节对齐,也就是说传入的不是16字节的倍数,会是什么情况?
测试代码
#import <malloc/malloc.h>
void *temp = calloc(1, 40);
NSLog(@"%lu", malloc_size(temp));
调用calloc方法,第二个参数传入40,注意这个值不是16的倍数。
运行结果:
通过结果可以看出,calloc里面也会进行16字节对齐,接下来我们来找找这16字节对齐的代码。
查看源码
首先先看看calloc函数在哪个源码中,用⌘+鼠标左键,属于malloc源码中
源码中大致的流程,如图:
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; // 16
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // (size + 16 -1)右移4位 SHIFT_NANO_QUANTUM=4
slot_bytes = k << SHIFT_NANO_QUANTUM; // 左移4位
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
这个函数就是16字节对齐的函数:
- 先判断size如果等于0,就给一个默认值16,
NANO_REGIME_QUANTA_SIZE
是个宏。 - 用size进行计算,size+16-1,将结果右移4位。然后在左移4位。目的是将低4位抹零。通过左右位移4位,来实现16字节对齐。