iOS对象的底层探索-alloc的原理的初步分析

350 阅读5分钟

image.png 我们在通过在此处下一个断点,然后通过汇编代码查看源码调用流程 image.png

通过源码断点调试: alloc-callAlloc-alloc-_objc_rootAlloc-callAlloc;我们发现callAlloc调用了两次 通过断点调试(不走源码,走汇编)的方式来探索为什么会走两次callAlloc; 这个我们后面会讲,现在放个图在这里,是调用了fixupMessageRef,里面当selector为alloc的时候imp转去调用objc_alloc,如图:

image.png

一、接下来我们通过汇编分析alloc的调用流程

1、alloc给对象调用alloc打断点 image.png 2、打开汇编调试 image.png 下面的是模拟器的结果,里面的跳转指令是callq image.png 下面是真机的跳转结果,走的是bl指令,我们接下来使用真机调试 image.png 3、给objc_alloc添加符号断点 image.png

image.png 跳转到下一步

image.png 来到objc_alloc的汇编代码,单步调试往下走,这里有两个跳转分别是_objc_rootAllocWithZone和objc_msgSend

image.png 跳转到了[NSObject alloc]里面调用的是objc_msgSend接下来进入objc_msgSend里面就直接跳转到了_objc_rootAlloc,如下: image.png

image.png 进入_objc_rootAlloc

image.png 我们进去 image.png 发现调用的是_objc_rootAllocWithZone方法; 然后我们结合源码

image.png 我们发现这个方法是通过_class_createInstanceFromZone返回对象的,我们再在此方法的汇编里面的ret处进行验证 image.png 通过上图我们发现在ret的汇编处的x0寄存器处确实生产了我们的LGPerson对象;

最后我们分析alloc的调用流程是

  1. alloc
  2. objc_alloc
  3. objc_msgSend
  4. alloc([NSObject alloc])
  5. _objc_rootAlloc
  6. _objc_rootAllocWithZone返回对象

通过上面的汇编代码分析我们发现,汇编里面没有调用callAlloc和_class_createInstanceFromZone; 此处就是因为编译器帮助我们进行了优化的部分,所以才看不到的

二、接下来我们去探索一下init方法的调用流程

image.png 直接返回了对象,我们也通过源码可以直接进行验正

image.png

image.png 通过源码分析init里面也是调用的_objc_rootInit->再直接返回了

我们看到+ init 直接返回了self 我们可能会有疑问,为什么直接返回了self了?苹果为什么要这么设计呢? 因为这是一种工厂设计模式,方便子类重写;

三、接下来我们在源码里面调试查看流程

1、在alloc处增加断点 image.png 2、启用objc_alloc等断点 image.png 来到objc_alloc image.png 3、继续来到callAlloc里面的objc_msgSend

image.png 4、继续来到了alloc方法,如下

image.png

image.png 5、来到了_objc_rootAlloc,继续进入有进入了callAlloc

image.png

image.png 通过源码我们看到调用流程也是

  1. alloc
  2. objc_alloc
  3. callAlloc里的objc_msgSend
  4. alloc([NSObject alloc])
  5. _objc_rootAlloc
  6. 走callAlloc里面的_objc_rootAllocWithZone方法
  7. 通过_class_createInstanceFromZone返回对象

我们可能会有疑问,在汇编调试调用里面没有走callAlloc和_class_createInstanceFromZone; 这些没有走的函数都是编译器帮我们优化掉了,简化了流程,就更快; 我们举个例子 我们可以看到在setting里面的optimization下面debug和release默认是不一样的,debug是None模式,优化较少,release是fastest、smalllest的 image.png 我们进入汇编调试一下

image.png

image.png 我们将编译器优化optimization设置和release一样

image.png 因为这里的a和b变量的值为使用就直接不在汇编里面显示了,直接去掉执行语句,简化流程。 我们在写一个函数sum

image.png

image.png 打印一下a试试

image.png

image.png

其实相当于将3+4的值放在a那里了,如

image.png

所以我们就解释了callAlloc和_class_creatInsanceFromZone在汇编里面没有调用的原因了

四、结构体对齐规则

结构体的内存对⻬⽅式

image.png 1:数据成员对⻬规则:结构(struct)的第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存

储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩的整数倍开始(⽐如int为4字节,则要从4的整

数倍地址开始存储)。

2:结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整

数倍地址开始存储.(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始

存储)。

3:收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果必须是其内部最⼤成员的整数倍,不⾜的要补⻬。 下面我们按照上面的规则验证 image.png 最后我们成功的打印出了sizeof对应的值

在调试过程中我也遇到了不少问题,汇总如下

1、为什么xcode在调试模式下面,lldb下面输入p直接就卡死了?

答:这个产生的问题原因也可以说是xcode的bug,在lldb下面需要切换为英文状态,否则xcode会崩,有遇到的小伙伴注意一下;

2、为什么有的模拟器走的指令和真机走的汇编指令不一样的,有的又是一样的?

答:原因是在一般情况下,模拟器走的是x86架构的,真机走的是arm64,它们的架构系统——汇编指令应该是不一样的;但是也有模拟器和真机都走arm架构的,就比如苹果新款M1款系列的电脑,它的模拟器走的就是和真机一样的arm架构;所以小伙伴如果你的调试的模拟器和别人的不一样也可能是正常的了,换个m1说不定就解决了,或者你用真机调试也是可以的;

tips:一些汇编和lldb调试的指令解释 b bl 跳转指令 -- 函数的调用 ret 函数的返回 ;分号表示注释 cpu里面包含寄存器、运算器、控制器 register read x0:读取x0里面的值,x0一般作存储函数的返回值,或者第一个参数,x1是第二个参数; po:打印对象,后面可以跟x0里面的值,有时候打印不出来,我们可以将后面的值强转为char*就可以了