iOS进阶ios进阶

OC对象原理(一) alloc&init探索

2019-12-22  本文已影响0人  長茳

iOS底层原理篇 主要是围绕底层进行源码分析-LLDB调试-源码断点-汇编调试,让自己以后回顾复习Runtime底层之美的😀😀
目录如下:
OC对象原理(一) alloc&init探索
OC对象原理(二) 内存对齐探索&malloc源码分析
持续更新中....更新有点慢请谅解....
文中有误的地方请指正,大家一起学习


准备条件(以我当前的配置为主)


alloc探索

我们先来看下面这段代码:

    TCJPerson *p1 = [TCJPerson alloc];
    TCJPerson *p2 = [p1 init];
    TCJPerson *p3 = [p1 init];

看完代码,有一个疑问就是:p1、p2、p3有什么联系呢?

为了弄清此问题,那就看运行结果呗:

通过运行结果,可以知道p1、p2、p3 他们所指的内存空间是一样的.也就是有三个不同的指针地址指向同一个内存空间.这正好从反向可以证明alloc才是创建对象-开辟内存,init只是一个初始化构造函数.
那为什么会是这样的呢?上面的allocinit到底做了什么呢?
我们先来看看alloc的实现:下面介绍三种方式来查看他的实现.(查看过程需用真机查看,因为模拟器查找的是x86_64环境和arm64是不一样的)

三种方式查看alloc的实现

第一种方式:直接下断点.
来到研究对象断点后(26行断点处),接下来按住control键和箭头所指的键会看到如下图所示: 继续之前的操作(按住control键和箭头所指的键)之后会跳转到如下图所示:

到此我们可以看到objc_alloclibobjc.A.dylib这个动态库里面.

第二种方式:下断符号断点.

第一步点左下角的+号按钮之后,在点击Symbolic Breakpoint

第二部添加alloc符号断点:

之后过掉断点之后会显示如下:

第三种方式:通过汇编查看.

如何操作如下图箭头所示:

之后会显示下图所示:

这时我们可以看到在22行有objc_alloc,那在此处打下断点按住control键和图上键头所指的键结果所下:

之后我们继续之前的操作(按住control键和箭头所指的键)结果如下:

通过这三种方式我们可以知道objc_alloclibobjc.A.dylib这个动态库里面,那接下来我们来通过alloc的源码来分析.在这之前,我们用寄存器来读取一下:那么什么是寄存器呢?
寄存器就是应该存储一些指针的一些东西,因为汇编它就是利用寄存器,用的妥妥的.过掉第一个断点(37行断点),来到alloc断点如下:

其中x0~x7用于程序调用的参数传递,x0是第一个参数的传递者也是返回的时候返回值的存储地方.因此我们一般读x0就可以了.之后过掉alloc断点来到_objc_rootAlloc断点:

objc_msgSend打上断点,来到断点处:

到此处发现汇编很难跟,一不小心就过了,为此我们还是用源码来跟吧.(嘿嘿汇编有点皮,搞不定😊😊)

alloc源码分析

打开可编译的objc756.2源码,通过前面的探究我们可以看到,在调用alloc之前还调用了objc_alloc,我们打下断点一步一步去看,图如下:

objc_alloc 方法:

我们先来到这个断点之处,然后全局搜索objc_alloc,如下图打上断点,之后我们过掉上图的断点,回来到下面的断点之处,这时我们看到allocWithZone返回的是false.在下图中我做了详细的解释.

那么为什么objc_alloc这个流程只会走一次呢?请看下图

从上图中可以看到符号绑定的操作是在fixupMessageRef这个方法里面实现的.而fixupMessageRef的调用又是在_read_images里面调用的.也就是dyld读取我们的镜像文件的时候.然而,在我们读取镜像文件的时候,系统会判断是否需要FIXUP,如果需要的话,我们就会调用fixupMessageRef,然后在fixupMessageRef内部判断当前的消息sel是否是SEL_alloc,如果是的话就替换其IMP为objc_alloc.其中FIXUP只会走一次,也就是说objc_alloc只会走一次.
之后会继续走callAlloc方法:在这个方法中如下图所示allocWithZone返回false,之后在走alloc方法.


补充另一种方法说明objc_alloc只会走一次:
  1. 先说说为什么会走objc_alloc?因为在LLVM的底层会调用CGF.EmitCallOrInvoke函数.

  2. 在说说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方法:
_objc_rootAlloc方法:
callAlloc方法:

此方法内部有一系列的判断条件,其中由于方法canAllocFast()的内部调用了bits.canAllocFast(),其返回值为固定值false,所以可以确定之后创建对象会走class_createInstance方法


class_createInstance方法:

进入class_createInstance方法,其内部调用了_class_createInstanceFromZone方法,并在其中进行size计算,内存申请,以及isa初始化:

_class_createInstanceFromZone方法:

我们先来看看对象size的计算,通过方法cls->instanceSize(extraBytes),计算出size,其中64位系统下,对象大小采用8字节对齐,但是实际申请的内存最低为16字节,也就是说系统分配内存按照16字节对齐分配

在这过程中还有两个核心重点:

经过calloc函数创建了一个指针,这个指针是怎么创建的,这个源码在:libmalloc.(具体的分析在OC对象原理(二) 内存对齐探索&malloc源码分析文中有写)

最后根据不同的条件,使用calloc或者malloc_zone_calloc进行内存申请,并且初始化isa指针,至此size大小的对象obj已经申请完成,并且返回.


init源码分析

进入init方法:

通过源码可以看到,其实init方法什么事情都没有做。那为什么init会什么都不做呢?
其实这是一种设计模式,自己思考一下,日常开发过程中,我们会在什么情况下,进行init方法的使用。—— 重写
在重写默认初始化的时候,我们可以根据自己的需求,进行各种个性化的设置。
工厂设计,父类没有执行,交给子类去实现

至此,我们在回到前面的问题?就很好的知道p1、p2、p3他们的内存地址为什么一样了吧.

扩展new的底层实现

进入new方法:

通过源码:在new方法里面就是调用alloc的实现(callAlloc)后 进行了init操作,由此可见,[Class new] 完全等价于 [[Class alloc] init].

总结

最后附上alloc流程图一张 alloc流程图.png
上一篇 下一篇

猜你喜欢

热点阅读