我们的类在创建一个对象时平时开发都是通过alloc init或者new来创建,那么我们就会想为什么会是alloc init,如果是这样创建呢
通过打印我么发现p, p1, p2三者都指向同一个地址,也就是说他们是同一个对象。可见,在 MyClass 使用 alloc 方法从系统中申请开辟内存空间后 init方法并没有对内存空间做任何的处理,地址指针的创建来自于 alloc方法。
细心的你一定注意到了,p, p1, p2都是相差了8个字节。 这是因为,指针占内存空间大小为8字节,p, p1, p2 都是从栈内存空间上申请的,且栈内存空间是连续的。同时,他们都指向了同一个内存地址。
那么alloc和init在底层分别做了什么事情呢
汇编查找
我们先通过汇编查看一下,首先在MyClass alloc 处打个断点,勾选Xcode -> Debug -> Debug Workflow -> Always Show Disassembly,运行
下符号断点“alloc”
接下来就来到此处
一个名为 libobjc.A.dylib 的库,至此,我们就应该要去找苹果开源的库,以寻找我们想要的答案。
源码分析
接下来我们通过源码分析,打开源码objc4-838.1(提取码: inb9) 查找一下,通过编译源码断点调试一路查找如下
alloc方法会调用到此处
接着是调用callAlloc
然后objc_msgSend查找alloc方法
接着是 调用 _objc_rootAlloc
第二次调用callAlloc
到此时我们发现callAlloc执行了两次第一次方法内部执行了最后一行objc_msgSend查找alloc方法,第二次执行了_objc_rootAllocWithZone,那么为什么会执行两次呢, 原因通过继续查找进去发现是因为fastpath(!cls->ISA()->hasCustomAWZ()) 这个第一次条件不成立才会执行objc_msgSend,这个判断是判断cls是否被初始化以及是否自定义了allocWithZone方法。
继续查找,接着执行了_class_createInstanceFromZone
到此时我们发现alloc方法创建对象是通过_class_createInstanceFromZone创建出来的,此时我们可以总结出alloc的底层执行流程如下图
init & new
接下来我们可以看init和new分别作了什么事情
从源码我们可以看到,init方法并没有做任何操作,而new方法其实本质上就是alloc + init,那么init方法没有做任何操作为什么还这样设计呢,其实苹果这样做的原因就是让我们可以重写init来初始化一些我们自己的操作,比如初始化UI,数据请求等等
编译器优化
在汇编里面我们并没有发现_class_createInstanceFromZone方法,这个是因为编译器对代码的优化,编译器的优化等级可以在targets->Build Setting->Optimization Level下修改
None的优化等级并不代表编译器不会优化,只是说None的优化等级没有那么高而已
alloc内存申请(对象的字节对齐方式)
接下来我们看一下被编译器优化掉的_class_createInstanceFromZone方法的内部做了什么操作。在方法内部我们看到有这样一句代码
点击进入方法内部发现有这样一句代码if (size < 16) size = 16; 也就是说通过allo或者new创建出来的对象最小的大小是16个字节,我们再看一下字节对齐算法
无缓存时
有缓存时
从上图的算法及WORD_MASK宏定义得知在我们iOS中64位的操作系统下创建对象开辟内存是以8个字节来进行对齐的,32位系统下是4字节对齐,如果有缓存时则以16字节对齐,如此那么我们如何知道我们创建的对象到底是多大呢,我们返回_class_createInstanceFromZone内查看发现 size = cls->instanceSize(extraBytes); 只是计算对象开辟需要的内存大小,系统实际为对象分配的内存大小依赖的是calloc函数
而calloc函数是以16字节对齐的,至此我们得出对象分配内存在计算对象需要的内存使用的是8字节对齐,实际分配内存使用的是16字节对齐。此时现在有一个疑问,对象的内存分配系统为什么要这样处理呢,这就涉及了对象的本质,等下我们再研究,继续往下阅读源码发现_class_createInstanceFromZone方法在calloc后又执行了以下代码
打印执行前后的obj
从打印结果我们可以看出obj->initInstanceIsa方法做了一个操作,把calloc返回的内存地址关联到响应的类。至此我们可以得出_class_createInstanceFromZone方法内部主要做了以下几个操作
对象的本质
首先我们创建一个mac app项目,在main文件下创建一个这样一个类
然后打开终端进入当前项目下输入clang -rewrite-objc main.m 指令,这样我们在文件夹下就得到了一个.cpp的文件,打开.cpp文件搜索MyClass我们可以看到
此时发现对象的本质是一个objc_object的结构体,其结构体内存储的是isa指针 + 成员变量的值。
1、那么为什么要字节对齐是因为字节是内存的容量单位。但是,CPU在读取内存的时候,却不是以字节为单位来读取的,⽽是以“块”为单位读取的,所以⼤家也经常听到⼀块内存,“块”的⼤⼩也就是内存存取的⼒度。如果不对⻬的话,在我们频繁的存取内存的时候,CPU就需要花费⼤量的精⼒去分辨你要读取多少字节,这就会造成CPU的效率低下,如果想要CPU能够⾼效读取数据,那就需要找⼀个规范,这个规范就是字节对⻬。
2、为什么对象内部的成员变量是以8字节对⻬,系统实际分配的内存以16字节对⻬?
以空间换时间。苹果系统采取16字节对⻬,这样分配是因为OC的对象中,第⼀位叫isa指针,它是必然存在的,⽽且它就占了8位字节,就算对象中没有其他的属性了,也⼀定有⼀个isa,那对象就⾄少要占⽤8位字节。如果以8位字节对⻬的话,如果连续的两块内存都是没有属性的对象,那么它们的内存空间就会完全的挨在⼀起,是容易混乱的。以16字节为⼀块,这就保证了CPU在读取的时候,按照块读取就可以,效率更⾼,同时还不容易混乱。
结构体对齐方式
我们知道了对象的本质是一个结构体,对象的内存对齐方式我们知道了,那么结构体的内存对齐是什么规则呢,通过打印以下几个结构体大小得知
struct1、struct2、struct3的大小分别是24、16、48个字节。也就是说在结构体内,变量位置不同或变量类型不同都会对结构体的大小产生影响,上面的结果我们也可以通过下面的规则计算出来
首先struct1、struct2内部没有结构体作为成员,所以我们通过规则1和规则3计算如下
而struct3内部有struct1这个结构体作为成员变量,所以我们通过规则1、2、3计算如下