iOS底层原理 01:OC对象原理探索(上)

2,554 阅读7分钟

这是我参与更文挑战的第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

我们发现结果一摸一样,由此我们可以得出以下结论:\

  1. alloc会创建一块内存
  2. init不会对当前的指针做任何操作
  3. 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的符号断点

符号断点定位查看调用流程

image.png

然后继续执行,我们发现进入了objc_alloc的实现,然后还知道了接下来将会执行_objc_rootAllocWithZone

image.png

我们也可以继续添加_objc_rootAllocWithZone的符号断点,继续查看流程

image.png

汇编查看调用流程

除了以上两种方法以外,我们还有第三中方法,利用汇编查看调用流程(先删除之前添加的符号断点)

1、设置断点,运行项目

2、打开汇编窗口Debug->Debug Workflow->Always Show disassembly

image.png

3、继续control+step into,执行到objc_alloc里边去

然后,添加相应的符号断点继续查看调用流程,再执行符号断点的时候,我们可以知道当前符号断点在源码的那个地方 image.png

除此之外,我们也可以直接添加alloc符号断点调试

image.png

既然知道了方法在源码中的位置,那么我们就可以通过源码更直接的去探索底层调用流程

汇编结合源码调试分析

苹果源码下载地址:
Apple Open Source
Source Browser

我们这里以objc4-818.2版本的源码为例进行探索,我们在上文已经知道了,[Person alloc]将会先调用[NSObject alloc]方法,那么我们可以在源码中找到此方法的实现的地方:

image.png

我们一步一步深入之后发现方法调用流程为alloc->_objc_rootAlloc->callAlloc,然后发现在callAlloc中,代码出现了判断分支调用

image.png

此时,我们无法准确判断出代码的调用流程,那么我们可以在工程中,依次添加alloc,_objc_rootAlloc,callAlloc三个符号断点(先将符号断点置为不可用):

image.png

运行项目至[Peson alloc]断点处,此时打开三个符号断点:

image.png

执行代码,我们发现调用顺序如下:

image.png

image.png

image.png

更深层次的调用,可以自行step into

在上述符号断点执行过程中,我们发现callAlloc这个符号没有被断点到,这是为什么呢?

这里有一个编译器优化的概念

编译器优化

什么叫做编译器优化呢,我们来看一段代码:

image.png

此时,我们切换到汇编窗口:

image.png

继续执行汇编代码:

image.png

接下来,我们执行到sum函数结束:

image.png

我们得出结论:经过一系列汇编运算,最终运算结果在w0(x0)寄存器中返回
接下来我们对编译器进行优化之后,我们再来看这个运算流程

设置编译器优化

image.png

Debug模式下,编译器默认没有优化
Release模式下,编译器优化为Fastest,Smalest[-Os]

我们将Debug模式下编译器优化也调整Fastest,Smalest[-Os]

image.png

然后运行项目,切换到汇编窗口

image.png

我们发现,没有sum方法调用的流程了,编译器直接将sum方法的运算结果存放在了w8寄存器,这就是编译器优化的结果

alloc的主线流程

1、准备工作

在源码工程中创建Person类,在main方法中调用[Person alloc]方法

image.png

2、断点执行

经过allocobjc_rootAlloc,最终进入callAlloc方法

image.png

image.png

image.png

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方法,结果也确实如此:

image.png 随后进入_class_createInstanceFromZone方法

image.png

image.png

4、cls->instanceSize先计算出需要的内存空间大小(16字节)

image.png

5、calloc申请内存空间

image.png

我们初始化的obj,系统默认为我们分配了一块脏内存空间,内存的数据只会覆盖不会清除 image.png 执行过calloc方法之后,系统为我们申请了新的内存空间,根据打印信息我们发现,当前的obj还没有和我们的Person关联上,我们继续执行

6、initInstanceIsaobjPerson绑定

image.png

执行过obj->initInstanceIsa(cls, hasCxxDtor)之后,objcls存在了绑定关系

至此,alloc底层调用逻辑已经完成,绘制流程图如下:

image.png

init和new分析

init

通过源码分析可知

init在源码中直接返回了obj,没有做多余的操作 init是一个工厂模式,作为析构函数,给子类重写使用,提供接口便于扩展

new

new相当于alloc init操作

字节对齐及其原理

上文说道cls->instanceSize会计算出需要的内存大小,那么如何计算所需内存大小呢?我们进入此方法:

image.png 断点执行[Person alloc]时,我们可以看到extraBytes值为0,那么所需内存大小size 就由alignedInstanceSize决定:

image.png

这里的unalignedInstanceSize8字节,来源于Person的父类NSObject中的Class isa

image.png

那么8字节是如果得出最终的16字节大小呢?我们看一下word_align的实现:

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

#define WORD_MASK 7UL可知WORD_MASK7 那么计算变为

(8 + 7) & ~715 & ~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

image.png

最终计算结果 if (size < 16) size = 16可知,size小于16时,结果取16

对象的内存空间

那么对象占用内存空间的大小,有什么决定呢?

对象占用内存空间的大小,由其成员变量决定 验证过程如下:

1、删除项目中所有断点

image.png

打印可知对象占用了8个字节,(内存中数据以16字节对齐)

2、添加一个属性

image.png

3、继续添加属性

image.png

结论:成员变量越多,占用内存空间越大

那么job1去哪了呢?

image.png

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 按浮点数格式显示变量