前言
- 本文底层使用的是objc 4-838.1版本
- 汇编指令
- b bl 跳转指令,函数的调用
- ret 函数的返回
- ; 注释
1、alloc方法在底层的调用流程
从上图的例子中我们可以发现,p1和p2两个指针的地址是一模一样的,那么我们可以得出一个结论:
p1 = p ,p2 = p
那么说明init方法并没有开辟内存空间,内存空间在alloc时已经开辟完成
那么alloc内具体干了啥呢?我们接下来结合源码和汇编来进行探索。
打开objc源码,经过搜索可以发现,alloc方法的调用是alloc -> _objc_rootAlloc -> callAlloc
但实际上,通过断点调试,会发现实际上的并没有直接调用alloc方法,这个时候我们通过汇编的方式来查看具体原因
可以发现,在调用alloc方法后,实际上是调用了objc_alloc方法,在源码中搜索,可以发现当有调用alloc方法时,实际上会调用objc_alloc
继续汇编的探索,打断点进入objc_alloc,可以发现,调用了objc_msgSend,而这个消息发送函数发送的就是alloc方法
打断点进入alloc方法,发现它实际上又去调用了_objc_rootAlloc方法
进入到_objc_rootAlloc后,可以发现跳转到了_objc_rootAllocWithZone
在源码中搜索_objc_rootAllocWithZone,发现方法返回了一个id类型
进入_class_createInstanceFromZone,可以发现返回的是一个object
在汇编中进入_objc_rootAllocWithZone,通过打印可以发现返回的就是一开始例子中的Person类对象
整理下上面的探索流程,可以发现具体的alloc底层调用方法顺序为:
经过上面的探索,我们能进一步的得出一个结论:
alloc方法开辟内存空间并返回了实例化对象
那么这就引出另一个问题了,alloc把对象都创建完返回了,那init方法又干了啥呢,它有何作用呢?在源码中可以发现,init方法啥都没干,直接返回了对象。
那苹果设计init方法的作用是啥呢?其实这是一种工厂模式,苹果的初衷是让开发者重写init方法,在内部进行一些初始化等相关操作。
2、编译器的优化
在上面的汇编探索中,我们可以发现实际上有一些方法在汇编中并没有显示出来,比如callAlloc,_class_createInstanceFromZone,这里就涉及到编译器的优化问题了。
在Build Settings中的Optimization Level选项中,可以看到编译器在Release下是Fastest,Smallest,当Debug模式下也是Fastest,Smallest时,再进行汇编探索会发现代码少了很多,这就是编译器的优化。None的优化等级并不代表编译器不会优化,只是说None的优化等级没有那么高而已。
从此也可以联想到方法的消失本质上就是被编译优化掉了。
关于编译器的优化,除了xcode的设置,还有代码方面:
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
fastpath 和 slowpath 都是一个宏定义,这个代码的作用是告诉编译器fastpath内的语句的执行可能性更大,这时候编译器就会预先读取fastpath这个条件下的语句,系统在运行时就会减少重新取指,从而减少指令跳转带来的性能上的下降。
3、对象的内存对齐方式
那么接下来我们探索下被优化掉的_class_createInstanceFromZone方法具体干了啥。
首先可以发现调用了instanceSize方法,代码中有一句if (size < 16) size = 16,也就是说当对象被alloc出来后,其初始化的大小就是16个字节。
方法内有一个缓存判断方法,
-
没缓存时,调用了方法alignedInstanceSize,alignedInstanceSize又直接调用word_align,可以发现,alignedInstanceSize本质就是个字节对齐算法。(x + 7) & ~7,低位清零,8字节对齐。
-
有缓存时,调用了方法fastInstanceSize,该方法调用了align16,可以发现align16是个16字节对齐算法
综上所述,我们可以知道,在64位操作系统下创建对象开辟内存是以8字节对齐,如果没缓存时是以16字节对齐,我们通过_class_createInstanceFromZone后续的代码可以发现,系统实际为对象分配大小依赖的的是calloc,instanceSize实际上只是计算对象开辟需要的内存大小,而calloc函数本身就是以16字节对齐。
继续往下看_class_createInstanceFromZone,obj调用了initInstanceIsa方法,这个方法实际上将obj绑定到相应的类上,我们可以通过打印来证明
总结一下,_class_createInstanceFromZone方法主要做了以下几个操作:
- 先计算出需要的内存空间大小,cls -> instanceSize
- 向系统申请开辟内存,返回地址指针,calloc
- 关联到相应的类,obj->initInstanceIsa
那苹果为啥要这么设计呢,我们接下来看下对象的本质
4、对象本质
首先创建一个mac app项目,创建一个类
@interface Person : NSObject
@property (nonatomic ,assign) int age;
@property (nonatomic ,copy) NSString *name;
- (void)test;
@end
@implementation Person
- (void)test {
}
@end
然后打开终端进入当前项目下输入clang -rewrite-objc main.m 指令,这样我们在文件夹下就得到了一个.cpp的文件,打开.cpp文件搜索Person我们可以发现对象的本质是一个objc_object的结构体,其结构体内存储的是 isa指针 + 成员变量的值。
那么为什么要字节对齐呢?
字节是内存的容量单位。但是,CPU在读取内存的时候,却不是以字节为单位来读取的,⽽是以
“块”为单位读取的,所以⼤家也经常听到⼀块内存,“块”的⼤⼩也就是内存存取的⼒度。如果不
对⻬的话,在我们频繁的存取内存的时候,CPU就需要花费⼤量的精⼒去分辨你要读取多少字节,
这就会造成CPU的效率低下,如果想要CPU能够⾼效读取数据,那就需要找⼀个规范,这个规范就
是字节对⻬。
那为什么对象成员变量是8字节对齐,而实际分配的内存是16字节对齐?
这其实是一种空间换时间的做法。苹果采取16字节对⻬,是因为OC的对象中,第⼀位叫isa指针,它是必然存在的,
⽽且它就占了8位字节,就算对象中没有其他的属性了,也⼀定有⼀个isa,那对象就⾄少要占⽤8
位字节。如果以8位字节对⻬的话,如果连续的两块内存都是没有属性的对象,那么它们的内存空
间就会完全的挨在⼀起,是容易混乱的。以16字节为⼀块,这就保证了CPU在读取的时候,按照块
读取就可以,效率更⾼,同时还不容易混乱。
5、结构体的内存对齐方式
通过上文我们已经知道对象的本质是一个结构体,那接下来我们看下结构体对象对齐方式
通过上面的例子,我们可以发现在结构体内,变量位置不同或变量类型不同都会对结构体的大小产生影响。
具体规则如下:
- 数据成员对⻬规则
结构(struct)的第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储)。 - 结构体作为成员
如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储)。
注意:结构体作为成员时,不是看做整体计算,而是以内部成员计算 - 收尾⼯作
结构体的总⼤⼩,也就是sizeof的结果必须是其内部最⼤成员的整数倍,不⾜的要补⻬。
那么看回上面的例子,我们可以分析为
- struct1
struct Struct1 {
double a;//8字节,0~7
char b;//1字节,8
int c;//4字节,4字节的整数倍,12~15
short d;//2字节,16~17
}struct1;//最大成员变量的整数倍,也就是double,3x8=24
- struct2
struct Struct2 {
double a;//8字节,0~7
int b;//4字节,8~11
char c;//1字节,12
short d;//2字节,2字节的整数倍,14~15
}struct2;//最大成员变量的整数倍,也就是double,2x8=16
- struct3
struct Struct1 {
double a;//8字节,24~31
char b;//1字节,32
int c;//4字节,4字节的整数倍,36~39
short d;//2字节,40~41
}struct1;
struct Struct3 {
double a;//8字节,0~7
char b;//1字节,8
int c;//4字节,4字节的整数倍,12~15
short d;//2字节,16~17
int e;//4字节,4字节整数倍,20~23
struct Struct1 str;
}struct3;//最大成员变量的整数倍,也就是double,6x8=48
6、总结
- alloc方法底层调用流程:
alloc -> objc_alloc -> callAlloc -> objc_msgSend -> alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone - cls -> instanceSize() - 计算对象需要的内存大小
calloc() - 系统实际为对象分配内存的大小 - 字节对齐是为了让CPU更高效的读取数据,因为CPU是按块读取,因此对象内部的成员变量是以8字节对齐,系统实际分配的内存以16字节对齐。
- 对象的本质是objc_object结构体,⾥⾯存储isa指针和成员变量的值。
- 结构体的对齐方式
- 数据成员对⻬规则
结构(struct)的第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储)。- 结构体作为成员
如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储)。
注意:结构体作为成员时,不是看做整体计算,而是以内部成员计算- 收尾⼯作
结构体的总⼤⼩,也就是sizeof的结果必须是其内部最⼤成员的整数倍,不⾜的要补⻬。