OC对象原理(一) alloc&init探索
iOS底层原理篇 主要是围绕底层进行
源码分析
-LLDB调试
-源码断点
-汇编调试
,让自己以后回顾复习Runtime底层之美的😀😀
目录如下:
OC对象原理(一) alloc&init探索
OC对象原理(二) 内存对齐探索&malloc源码分析
持续更新中....更新有点慢请谅解....
文中有误的地方请指正,大家一起学习
准备条件(以我当前的配置为主)
- macOS 10.15.2
- Xcode 11.3
-
objc4-756.2
(如果你想自己配置可编译objc-756.2的源码,可以看看Cooci帅哥的这篇文章)
alloc探索
我们先来看下面这段代码:
TCJPerson *p1 = [TCJPerson alloc];
TCJPerson *p2 = [p1 init];
TCJPerson *p3 = [p1 init];
看完代码,有一个疑问就是:p1、p2、p3有什么联系呢?
![](https://img.haomeiwen.com/i2340353/740628c333c1291d.png)
通过运行结果,可以知道p1、p2、p3 他们所指的内存空间是一样的.也就是有三个不同的指针地址指向同一个内存空间.这正好从反向可以证明alloc
才是创建对象-开辟内存,init
只是一个初始化构造函数.
那为什么会是这样的呢?上面的alloc
和init
到底做了什么呢?
我们先来看看alloc
的实现:下面介绍三种方式来查看他的实现.(查看过程需用真机查看,因为模拟器查找的是x86_64环境和arm64是不一样的)
三种方式查看alloc的实现
第一种方式:直接下断点.
![](https://img.haomeiwen.com/i2340353/1f5ba0e225e61de5.png)
![](https://img.haomeiwen.com/i2340353/3ba64a704577074a.png)
![](https://img.haomeiwen.com/i2340353/e07155185aeeb18b.png)
到此我们可以看到objc_alloc
在libobjc.A.dylib
这个动态库里面.
第二种方式:下断符号断点.
第一步点左下角的+号按钮之后,在点击Symbolic Breakpoint
![](https://img.haomeiwen.com/i2340353/73c8718d742f7707.png)
第二部添加alloc符号断点:
![](https://img.haomeiwen.com/i2340353/96489aa9dfbe7d3e.png)
之后过掉断点之后会显示如下:
![](https://img.haomeiwen.com/i2340353/263dd63a245ac5d6.png)
第三种方式:通过汇编查看.
如何操作如下图箭头所示:
![](https://img.haomeiwen.com/i2340353/83049b5d24269036.png)
之后会显示下图所示:
![](https://img.haomeiwen.com/i2340353/d8ceaebce1768b9d.png)
这时我们可以看到在22行有objc_alloc
,那在此处打下断点按住control键和图上键头所指的键结果所下:
![](https://img.haomeiwen.com/i2340353/510cc82ec5c09763.png)
之后我们继续之前的操作(按住control键和箭头所指的键)结果如下:
![](https://img.haomeiwen.com/i2340353/67af14bc2cfbd370.png)
通过这三种方式我们可以知道objc_alloc
在libobjc.A.dylib
这个动态库里面,那接下来我们来通过alloc
的源码来分析.在这之前,我们用寄存器来读取一下:那么什么是寄存器呢?
寄存器就是应该存储一些指针的一些东西,因为汇编它就是利用寄存器,用的妥妥的.过掉第一个断点(37行断点),来到alloc
断点如下:
![](https://img.haomeiwen.com/i2340353/e244991f0b9f4a95.png)
其中x0~x7
用于程序调用的参数传递,x0
是第一个参数的传递者也是返回的时候返回值的存储地方.因此我们一般读x0就可以了.之后过掉alloc
断点来到_objc_rootAlloc
断点:
![](https://img.haomeiwen.com/i2340353/8cae02a53fc6ff4e.png)
在objc_msgSend
打上断点,来到断点处:
![](https://img.haomeiwen.com/i2340353/58f908376ed66899.png)
到此处发现汇编很难跟,一不小心就过了,为此我们还是用源码来跟吧.(嘿嘿汇编有点皮,搞不定😊😊)
alloc源码分析
打开可编译的objc756.2源码,通过前面的探究我们可以看到,在调用alloc之前还调用了objc_alloc,我们打下断点一步一步去看,图如下:
objc_alloc 方法:
![](https://img.haomeiwen.com/i2340353/863e5cf3b7608d4f.png)
我们先来到这个断点之处,然后全局搜索objc_alloc
,如下图打上断点,之后我们过掉上图的断点,回来到下面的断点之处,这时我们看到allocWithZone
返回的是false
.在下图中我做了详细的解释.
![](https://img.haomeiwen.com/i2340353/67fd4a0adfccddab.png)
那么为什么objc_alloc
这个流程只会走一次呢?请看下图
![](https://img.haomeiwen.com/i2340353/b0478f7ac8354a88.png)
![](https://img.haomeiwen.com/i2340353/02db6a0cc92289bb.png)
从上图中可以看到符号绑定的操作是在fixupMessageRef
这个方法里面实现的.而fixupMessageRef
的调用又是在_read_images里面调用的.也就是dyld读取我们的镜像文件的时候.然而,在我们读取镜像文件的时候,系统会判断是否需要FIXUP,如果需要的话,我们就会调用fixupMessageRef
,然后在fixupMessageRef
内部判断当前的消息sel是否是SEL_alloc,如果是的话就替换其IMP为objc_alloc.其中FIXUP只会走一次,也就是说objc_alloc只会走一次.
之后会继续走callAlloc方法:在这个方法中如下图所示allocWithZone返回false,之后在走alloc方法.
![](https://img.haomeiwen.com/i2340353/116d0c55fba5f734.png)
补充另一种方法说明
objc_alloc
只会走一次:
-
先说说为什么会走
objc_alloc
?因为在LLVM
的底层会调用CGF.EmitCallOrInvoke
函数. -
在说说
objc_alloc
只会走一次:- 正如前面说的第一次的时候
call
-objc_alloc
->none
没有返回对象;通过LLVM
源码可以看到:
- 正如前面说的第一次的时候
如果返回值为none
时,return CGF.EmitObjCAlloc(Receiver, CGF.ConvertType(ResultType))
也返回为none
,那么就会进行下面的条件判断if (Optional<llvm::Value *> SpecializedResult = tryGenerateSpecializedMessageSend(CGF, ResultType, Receiver, Args,Sel, Method, isClassMessage))
而此时并没有进入return RValue::get(SpecializedResult.getValue())
但是此时调用了objc_alloc
,之后会继续走return GenerateMessageSend(CGF, Return, ResultType, Sel, Receiver, Args, OID, Method)
调用alloc方法.
[图片上传失败...(image-774ba6-1577945076895)]
- 也可以这样说:走的是symbol符号绑定,没有走objc_msgSend
- 并不是我们调用的,而是系统调用符号的(编译器调用)
alloc方法:
![](https://img.haomeiwen.com/i2340353/25230976329e4aa5.png)
_objc_rootAlloc方法:
![](https://img.haomeiwen.com/i2340353/11bfa679be1a0811.png)
callAlloc方法:
此方法内部有一系列的判断条件,其中由于方法canAllocFast()的内部调用了bits.canAllocFast(),其返回值为固定值false,所以可以确定之后创建对象会走class_createInstance方法
![](https://img.haomeiwen.com/i2340353/bc980075b1281000.png)
class_createInstance方法:
进入class_createInstance
方法,其内部调用了_class_createInstanceFromZone
方法,并在其中进行size计算,内存申请,以及isa初始化:
![](https://img.haomeiwen.com/i2340353/dabe860ef4e7c26d.png)
_class_createInstanceFromZone方法:
![](https://img.haomeiwen.com/i2340353/bbe42f41504a9fff.png)
我们先来看看对象size
的计算,通过方法cls->instanceSize(extraBytes),计算出size,其中64位系统下,对象大小采用8字节对齐,但是实际申请的内存最低为16字节,也就是说系统分配内存按照16字节对齐分配
![](https://img.haomeiwen.com/i2340353/4222abeea5b9913e.png)
![](https://img.haomeiwen.com/i2340353/6980fbfc735437fa.png)
在这过程中还有两个核心重点:
obj = (id)calloc(1, size)
obj->initInstanceIsa(cls, hasCxxDtor)
- 这两行代码应该直接决定了
alloc
的作用
我们来对这两个核心内容分析一下:
创建指针,申请内存
经过calloc
函数创建了一个指针,这个指针是怎么创建的,这个源码在:libmalloc
.(具体的分析在OC对象原理(二) 内存对齐探索&malloc源码分析文中有写)
最后根据不同的条件,使用calloc
或者malloc_zone_calloc
进行内存申请,并且初始化isa
指针,至此size
大小的对象obj
已经申请完成,并且返回.
init源码分析
进入init方法:
![](https://img.haomeiwen.com/i2340353/9173f47c24eeff2f.png)
![](https://img.haomeiwen.com/i2340353/5e395d15f805f34e.png)
通过源码可以看到,其实init
方法什么事情都没有做。那为什么init
会什么都不做呢?
其实这是一种设计模式,自己思考一下,日常开发过程中,我们会在什么情况下,进行init
方法的使用。—— 重写
在重写默认初始化的时候,我们可以根据自己的需求,进行各种个性化的设置。
工厂设计,父类没有执行,交给子类去实现。
至此,我们在回到前面的问题?就很好的知道p1、p2、p3他们的内存地址为什么一样了吧.
扩展new的底层实现
进入new
方法:
![](https://img.haomeiwen.com/i2340353/c54fe2d27d237ee3.png)
通过源码:在new方法里面就是调用alloc的实现(callAlloc)后 进行了init操作,由此可见,[Class new] 完全等价于 [[Class alloc] init].
总结
- alloc创建了对象并且申请了一块不少于16字节的内存控件.
- init其实什么也没做,返回了当前的对象。其作用在于提供一个范式,方便开发者自定义.
- new其实就是调用alloc的实现(callAlloc)后 进行了init操作.
![](https://img.haomeiwen.com/i2340353/f6e6e9d868ca5f67.png)