OC对象的内存和指针
- 我们每天都在写
[[xxx alloc] init],但从未探究过alloc和init内部都干了什么?带着疑问,我们通过一段简单代码,先来看看OC对象的初始化:
FFObj *obj = [FFObj alloc];
FFObj *o1 = [obj init];
FFObj *o2 = [obj init];
NSLog(@"%@ -- %p -- %p", obj, obj, &obj);
NSLog(@"%@ -- %p -- %p", o1, o1, &o1);
NSLog(@"%@ -- %p -- %p", o2, o2, &o2);
- 通过上面的代码运行打印出的结果是:
<FFObj: 0x600003a28170> -- 0x600003a28170 -- 0x7ffee41dcc38
<FFObj: 0x600003a28170> -- 0x600003a28170 -- 0x7ffee41dcc30
<FFObj: 0x600003a28170> -- 0x600003a28170 -- 0x7ffee41dcc28
- 图示:
- 结论:
- 通过三个对象的内存地址可以看出,
alloc方法开辟了内存,而init方法没有对内存做操作,这三个对象是一模一样的. - 通过三个指针地址可以看出,三个指针都指向了堆空间(0x6开头是堆)内的同一块区域,而三个指针自身则是在栈(0x7开头是栈)中开辟的三个连续空间.
那么,
alloc是如何开辟内存空间的呢?init又是否真的什么都没做呢?我们继续往下看.
- 通过三个对象的内存地址可以看出,
寻找alloc
查找alloc的位置以及底层源码调用流程,可以通过:
断点跟踪-
先将
alloc的调用位置添加断点 -
-
按住
control键点击下一步,跟踪查看: -
-
查找到关键函数
objc_alloc
-
汇编分析- 在断点位置,通过点击Xcode导航栏的
Debug->Debug Workflow->Always Show Disassembly,查看汇编代码: - 查找到关键函数
objc_alloc
- 在断点位置,通过点击Xcode导航栏的
通过已知函数符号断点,比如直接将alloc添加进符号断点,然后跟踪查看.- 得到关键函数
objc_alloc后,我们将它也加入到符号断点中:最终得知,
objc_alloc在libobjc.A.dylib中,而这正是objc框架的底层源码所在,好在这部分代码苹果是开源了的,我们可以下来源码,来继续探查.
源码地址
alloc源码流程分析
- 通过断点调试或者全局搜索找到
alloc方法 -
_objc_rootAlloc -
callAlloc - 到了
callAlloc之后,不再是简单的方法逐层封装调用了,而是出现了大量的逻辑,也就是说,我们要从这里开始分析源码逻辑了. - 补充说明
#if __OBJC2__表示该代码块内的代码属于objc2.0版本,也正是我们当前使用的版本.fastpath的宏定义为#define fastpath(x) (__builtin_expect(bool(x), 1)),表示当前的if判断,有更高的几率为trueslowpath的宏定义为#define slowpath(x) (__builtin_expect(bool(x), 0)),表示当前的if判断,有更高的几率为false__builtin_expect,用法为__builtin_expect(bool(x), y),表示布尔值bool(x)有更高的几率为y,能够让编译器在编译阶段将此处的代码跳转进行逻辑优化,得到更高性能的汇编代码.
-
_objc_rootAllocWithZone -
_class_createInstanceFromZone- 看来我们终于走到了
alloc流程中最核心的部分,该方法内部进行了内存的计算和分配逻辑 instanceSize计算所需内存空间的大小- 根据
if (zone)判断来调用malloc_zone_calloc或者calloc进行内存分配 - 在
calloc之前,分配给obj的是一块脏内存,执行calloc之后,obj才真的分配到了内存,calloc执行前后的obj内存见下图 - 此时
po出来的obj是只有内存地址而没有类型的,说明此时的obj没有绑定到类 - 通过
if (!zone && fast)判断分别调用obj->initInstanceIsa(cls, hasCxxDtor)或obj->initIsa(cls)来初始化isa,将obj绑定到类 - 通过
if (fastpath(!hasCxxCtor))判断,直接返回obj或者返回object_cxxConstructFromClass(obj, cls, construct_flags)
- 看来我们终于走到了
-
instanceSize- 当有缓存的时候,会走到
fastInstanceSize - 没有缓存的时候,会走到
alignedInstanceSize - 如果最终得到的
size < 16,则会返回16
-
alignedInstanceSizeword_align字节对齐算法中参数x的值取自unalignedInstanceSize(),即data()->ro()->instanceSize,实例变量的大小,由ivars决定.WORD_MASK的值在64位中为7,在32位中为3-
字节对齐算法说明(
64位)(x + WORD_MASK) & ~WORD_MASKx = 8WORD_MASK = 7(8 + 7) & ~7=15 & ~70000 1111 & ~0000 0111=0000 1111 & 1111 1000- 结果为
0000 1000=8 - 即
(x + y) & ~y计算的是y + 1的整数倍数, 等同于(x + y) >>z <<z,其中z是以2为底y的对数,即y = 8 时 z = 3 - 由此可知,
alignedInstanceSize()最终的计算结果是以8字节对齐,取8的倍数
- 那么,为什么要以
8字节对齐开辟内存呢?为什么最终分配内存如果<16要=16呢?- 单位长度最高为
8,比如指针,其他常用数据类型都可以被8以内的大小存储下来. - 恒定以
8为单位存储数据后,CPU也可以恒定以8为单位去读取数据,不需要不停地变更存取长度,这是通过以空间换时间的方式,提高CPU存取效率. - 以
16为最小开辟空间是因为类会至少有一个isa成员,而isa是结构体指针类型,长度为8,开辟出更多的空间时为了容错处理.
- 单位长度最高为
-
fastInstanceSize- 调用
align16实现16字节对齐
-
initInstanceIsainitInstanceIsa会调用initIsa
-
initIsainitIsa会对isa进行绑定if (!nonpointer)判断表示是否进行指针优化