这是我参与更文挑战的第1天,活动详情查看: 更文挑战
alloc对象的指针地址和内存
在开始研究之前,我们先创建一个工程,然后新建一个类Person
,然后我们看一下下边代码的结果
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
NSLog(@"%@--%p--%p", p1, p1, &p1);
NSLog(@"%@--%p--%p", p2, p2, &p2);
NSLog(@"%@--%p--%p", p3, p3, &p3);
最终打印结果为:
<Person: 0x2809ec5b0>--0x2809ec5b0--0x16f9f1b58
<Person: 0x2809ec5b0>--0x2809ec5b0--0x16f9f1b50
<Person: 0x2809ec5b0>--0x2809ec5b0--0x16f9f1b48
我们发现结果一摸一样,由此我们可以得出以下结论:\
alloc
会创建一块内存init
不会对当前的指针做任何操作p1
p2
p3
三个指针地址为连续的,并且三个地址不一样的指针,指向了同一块内存空间 如下图所示:
那么,
alloc
是如何开辟内存的?init
真的什么都不做么?那么它有何用处? 要想解答这些问题,我们需要研究alloc
的底层实现,但是我们点击发现我们无法查看alloc
的具体实现,那么我们就要想其他方法来探索alloc
底层究竟是如何实现的。
底层探索的三种方法
- 1、
control
+step into - 2、符号断点定位查看调用流程
- 3、汇编查看调用流程
control
+step into
1、我们在Person *p1 = [Person alloc];
代码处下断点,然后运行项目
2、此时,我们按住control
键,然后点击step into
按钮
此时,我们来到了objc_alloc
方法,下图为真机演示,模拟器有所区别不做展示
继续下一步我们也无法获取更多信息,此时,我们需要添加一个objc_alloc
的符号断点
符号断点定位查看调用流程
然后继续执行,我们发现进入了objc_alloc
的实现,然后还知道了接下来将会执行_objc_rootAllocWithZone
我们也可以继续添加_objc_rootAllocWithZone
的符号断点,继续查看流程
汇编查看调用流程
除了以上两种方法以外,我们还有第三中方法,利用汇编查看调用流程(先删除之前添加的符号断点)
1、设置断点,运行项目
2、打开汇编窗口Debug
->Debug Workflow
->Always Show disassembly
3、继续control
+step into
,执行到objc_alloc
里边去
然后,添加相应的符号断点继续查看调用流程,再执行符号断点的时候,我们可以知道当前符号断点在源码的那个地方
除此之外,我们也可以直接添加alloc
符号断点调试
既然知道了方法在源码中的位置,那么我们就可以通过源码更直接的去探索底层调用流程
汇编结合源码调试分析
苹果源码下载地址:
Apple Open Source
Source Browser
我们这里以objc4-818.2
版本的源码为例进行探索,我们在上文已经知道了,[Person alloc]
将会先调用[NSObject alloc]
方法,那么我们可以在源码中找到此方法的实现的地方:
我们一步一步深入之后发现方法调用流程为alloc
->_objc_rootAlloc
->callAlloc
,然后发现在callAlloc
中,代码出现了判断分支调用
此时,我们无法准确判断出代码的调用流程,那么我们可以在工程中,依次添加alloc
,_objc_rootAlloc
,callAlloc
三个符号断点(先将符号断点置为不可用):
运行项目至[Peson alloc]
断点处,此时打开三个符号断点:
执行代码,我们发现调用顺序如下:
更深层次的调用,可以自行
step into
在上述符号断点执行过程中,我们发现callAlloc
这个符号没有被断点到,这是为什么呢?
这里有一个编译器优化的概念
编译器优化
什么叫做编译器优化呢,我们来看一段代码:
此时,我们切换到汇编窗口:
继续执行汇编代码:
接下来,我们执行到sum
函数结束:
我们得出结论:经过一系列汇编运算,最终运算结果在
w0(x0)
寄存器中返回
接下来我们对编译器进行优化之后,我们再来看这个运算流程
设置编译器优化
Debug
模式下,编译器默认没有优化
Release
模式下,编译器优化为Fastest,Smalest[-Os]
我们将Debug
模式下编译器优化也调整Fastest,Smalest[-Os]
然后运行项目,切换到汇编窗口
我们发现,没有
sum
方法调用的流程了,编译器直接将sum
方法的运算结果存放在了w8
寄存器,这就是编译器优化的结果
alloc的主线流程
1、准备工作
在源码工程中创建Person
类,在main
方法中调用[Person alloc]
方法
2、断点执行
经过alloc
,objc_rootAlloc
,最终进入callAlloc
方法
3、继续执行
调用_objc_rootAllocWithZone
这里有两个宏
slowpath:#define slowpath(x) (__builtin_expect(bool(x), 0))
fastpath:#define fastpath(x) (__builtin_expect(bool(x), 1))
作用为:允许程序员将最有可能执行的分支告诉编译器
__builtin_expect((x),1)
表示 x 的值为真的可能性更大
__builtin_expect((x),0)
表示 x 的值为假的可能性更大
根据宏定义的含义,大概率会执行objc_rootAllocWithZone
方法,结果也确实如此:
随后进入_class_createInstanceFromZone
方法
4、cls->instanceSize
先计算出需要的内存空间大小(16字节)
5、calloc
申请内存空间
我们初始化的
obj
,系统默认为我们分配了一块脏内存空间,内存的数据只会覆盖不会清除 执行过calloc
方法之后,系统为我们申请了新的内存空间,根据打印信息我们发现,当前的obj
还没有和我们的Person
关联上,我们继续执行
6、initInstanceIsa
将obj
与Person
绑定
执行过
obj->initInstanceIsa(cls, hasCxxDtor)
之后,obj
与cls
存在了绑定关系
至此,alloc
底层调用逻辑已经完成,绘制流程图如下:
init和new分析
init
通过源码分析可知
init在源码中直接返回了
obj
,没有做多余的操作 init是一个工厂模式,作为析构函数,给子类重写使用,提供接口便于扩展
new
new
相当于alloc init
操作
字节对齐及其原理
上文说道cls->instanceSize
会计算出需要的内存大小,那么如何计算所需内存大小呢?我们进入此方法:
断点执行[Person alloc]
时,我们可以看到extraBytes
值为0
,那么所需内存大小size
就由alignedInstanceSize
决定:
这里的unalignedInstanceSize
是8
字节,来源于Person
的父类NSObject
中的Class isa
那么8
字节是如果得出最终的16
字节大小呢?我们看一下word_align
的实现:
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
#define WORD_MASK 7UL
可知WORD_MASK
为7
那么计算变为
(8 + 7) & ~7
即
15 & ~7
计算过程如下:
0000 1111 (15的二进制 )
0000 0111 (7的二进制)
1111 1000 (~7的二进制)
15 & ~7 与运算如下
0000 1111
1111 1000
结果
0000 1000 转换为二进制是8
类似
(8 + 7) >> 3 << 3
8字节对齐,取8的整数,
alignedInstanceSize
结果为8
最终计算结果
if (size < 16) size = 16
可知,size
小于16
时,结果取16
对象的内存空间
那么对象占用内存空间的大小,有什么决定呢?
对象占用内存空间的大小,由其
成员变量
决定 验证过程如下:
1、删除项目中所有断点
打印可知对象占用了
8
个字节,(内存中数据以16字节对齐)
2、添加一个属性
3、继续添加属性
结论:成员变量越多,占用内存空间越大
那么job1
去哪了呢?
x/4gx p
:以4
个格式化的排版打印对象p
更多解释:
x/nuf <addr>
n 表示要显示的内存单元的个数
------------------------
u 表示一个地址单元的长度
b 表示单字节
h 表示双字节
w 表示4字节
g 表示8字节
------------------------
f 表示显示方式,可取以下值:
x 按十六进制格式显示变量
d 按十进制格式显示变量
u 按十进制格式显示无符号整型
o 按八进制格式显示变量
t 按二进制格式显示变量
a 按十六进制格式显示变量
i 按指令地址格式显示变量
c 按字符格式显示变量
f 按浮点数格式显示变量