iOS Developer

iOS底层原理:OC对象底层探索之alloc初探

2022-01-19  本文已影响0人  蒲公英少年
0-0.png

iOS开发的小伙伴们对 [XXX alloc] init] 都不陌生,可以说 allocinit 贯穿我们整个的开发过程中。那么在OC对象的底层,到底做了哪些操作呢?今天我们就来探索一下 alloc 底层的工作流程。

一、抛砖引玉

我们先来看一下下面这张图中的测试代码和打印结果:


1.png

从上面的打印结果来看,p、p1、p2对象的内存地址是一样的,但是p、p1、p2对象的指针地址(&p、&p1、&p2)是不同的。而pNew对象的内存地址和指针地址和p、p1、p2都不一样,很显然,pNew属于拥有另一块内存空间的另一个对象了。
由此我们暂时得出结论:

结合堆栈的知识 ,我画了下面👇这张图,帮助大家理解。

2.png

二、准备工作

通过上面我们可以发现,对象内存地址是通过 alloc 创建,我们看一下 alloc 是怎么实现的。
点击 alloc 方法进入 NSObject.h:

2.1.png 2.2.png

进入NSObject.h,我们再点击跳转,发现跳转不进去了,也就看不到alloc的实现了。难道我们就只能停在这里?就只能在外面蹭一蹭了吗?
NO,下面来介绍一下探索底层的三种方法,方便我们在探索底层源码的时候能够顺利的跟对方法(函数)的一个执行流程。

第一种:添加符合断点方式
3.png 4.png 5.png 6.png 7.png 8.png

到这里,我们可以看到alloc方法在libobjc.A.dylib库中(ps:libobjc.A.dylib是objc的系统库,感兴趣的小伙伴可以去苹果开源官网Open Source下载查看,注意:Open Source上下载下来的源码是不能直接编译和调试的,想要下载的objc源码可编译调试的小伙伴可以移步到我之前的文章iOS底层原理(一):苹果开源 objc4-818 源码项目的编译和调试

第二种: 断点 + step into方式
9.png 10.png 11.png

这里我们可以看到,断点进入了libobjc.A.dylib中的objc_alloc函数,由此可知alloc方法的源码在libobjc.A.dylib库中。

第三种: 汇编跟进方式
12.png 13.png 14.png 15.png

好了,到此底层探索的三种方式就介绍完了,接下来我们步入正题吧!

三、alloc源码探索

好的,有了上面的探索方法,我们现在就拿 objc 源码项目来探索 alloc 的底层实现吧。
首先,打开之前编译好的 objc4-818.2 项目,需要的小伙伴可以参考我之前文章iOS底层原理(一):苹果开源 objc4-818 源码项目的编译和调试,到 Open Source 上下载源码自行编译,不想麻烦的也可以直接去 GitHub 上下载:JQObjc4-818.2BuildDebug
然后,找到 JQObjcBuildDemo 目录下创建一个JQPerson类。然后在main.m中添加如下代码:

16.png
注意: 这里 16、17行 分别有个断点,后面会用到!!!

我们从上面的底层探索方式中可以看到:[JQPerson alloc]在底层libobjc.A.dylib库中执行的objc_alloc方法,接下来我就来验证一下。

第1步:alloc 和 objc_alloc
17.png
18.png 19.png 20.png
这就验证了我们前面所讲的,alloc方法在底层libobjc.A.dylib库中执行的objc_alloc方法 21.png
那么为什么[JQPerson alloc]在底层会先走objc_alloc方法,再走alloc方法呢?按照我们在 objc 源码中看到的方法调用流程,应该是[JQPerson alloc] => alloc呀?

为了验证这个问题,我们需要请出YYDS(永远滴神):llvm源码(是苹果开源的系统级别的源码),看一看苹果是不是在这里面做了什么骚操作。llvm-project下载地址

第2步:llvm-project 底层分析

由于 llvm-project 项目比较大,这里我们用 VSCode 打开

21-1.png

我们主要看3号位置的方法解释,这里我翻译了一下,大家可以自行去看,这是苹果对性能的一个优化。主要意思就是:objc在运行时提供了快捷入口,这些入口比普通的消息发送速度更快,如果运行支持所需要的入口的话,这个方法就会调用并返回结果,否则返回None,调用者自己生成一个消息发送。

21-2.png

这个方法是运行时在底层的入口,所有的消息发送都会走这里。从代码可以看出,如果tryGenerateSpecializedMessageSend方法返回None,这里判断为false,就会走GenerateMessageSend方法,也就是调用者自己生成一个普通的msgSend

21-3.png

可以看到EmitObjCAlloc方法这里生成了一个objc_alloc的入口(ObjCEntrypoints),包装为emitObjCValueOperation被返回执行,并且llvm对此做一个标记存在Selector中,而Selector则记录在SelectorTable

21-4.png
21-5.png
21-6.png
21-7.png
21-8.png

由此可以验证:[JQPerson alloc]在底层会先走到objc_alloc

第3步:callAlloc

好了,allocobjc_alloc的调用清晰了。接着,我们来看一下最核心的方法callAlloc

22.png 23.png 24.png 25.png
26.png 27.png
28.png
29.png

这里我们就会奇怪,为什么JQPerson类再次alloc时,就直接走到if (fastpath(!cls->ISA()->hasCustomAWZ()))条件判断中的代码了呢?

30.png
31.png
32.png
33.png

由以上源码可以看出:
a. 当JQPerson类第一次调用alloc方法时,底层会先调用objc_alloc,此时callAlloc被第一次调用,callAlloc内部通过当前clsISA返回一个Class对象;
b. 紧接着会去判断当前ClasscacheFAST_CACHE_HAS_DEFAULT_AWZ(存储在元类metaclass中,记录着cache中是否已经缓存了alloc/allocWithZone:方法的实现)这个标志位的值是否为真,由于是第一次执行,没有缓存,所以cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ)取出来的值是false,前面加个,变成了truecallAllocif (fastpath(!cls->ISA()->hasCustomAWZ()))又加了个,所以值为false
c. 然后走到了if (allocWithZone),由于objc_alloc方法中allocWithZone参数传值为false,所以走到了(objc_msgSend)(cls, @selector(alloc))。然后,callAlloc被第二次调用,由于执行过了alloc方法,所以此时有了alloc的方法缓存,所以if (fastpath(!cls->ISA()->hasCustomAWZ()))判断为true,执行_objc_rootAllocWithZone
d. 最后就是 main.m 中第17行JQPerson类第二次调用alloc方法,此时由于JQPerson类的cache中已经有了缓存,FAST_CACHE_HAS_DEFAULT_AWZ这个标志位的值为真,也就是if (fastpath(!cls->ISA()->hasCustomAWZ()))这个条件为真,所以,会直接执行_objc_rootAllocWithZone
下面我画一下流程图,帮助小伙们理解一下:

[JQPerson alloc]流程图新.png

另外,这里我附一张NSObject alloc]的流程图,有兴趣的小伙们可以去试一试:

[NSObject alloc]流程图.png

这里NSObject alloc]只走了一遍callAlloc方法,猜测原因是:系统对 NSObject 做了优化,提前给cache添加了缓存。

好了,alloc的底层探索今天先写到这里。下面一篇文章我们将探索一下alloc开辟内存空间相关的源码。敬请期待吧!!!

上一篇 下一篇

猜你喜欢

热点阅读