iOS底层-OC对象原理(上)

背景

iOS开发,我们每天都在 alloc对象,但是我们jump to Definition到NSObject底层能看到的仅有个未实现的alloc方法,而我们对alloc底层的逻辑却一无所知。
alloc底层有什么、
alloc做了什么、
init做了什么、
allocinitnew到底有什么关系和区别呢?

掘金地址同步:掘金-OC对象原理(上)

实例

简单的代码和打印(分别输出对象的内容,对象的地址,以及对象指针的地址)示例:

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

ZMPerson *newP = [ZMPerson alloc];

NSLog(@"%@ - %p - %p",p1,p1,&p1);
NSLog(@"%@ - %p - %p",p2,p2,&p2);
NSLog(@"%@ - %p - %p",p3,p3,&p3);
NSLog(@"%@ - %p - %p",newP,newP,&newP);
<ZMPerson: 0x6000030f40e0> - 0x6000030f40e0 - 0x7ffeed054068
<ZMPerson: 0x6000030f40e0> - 0x6000030f40e0 - 0x7ffeed054060
<ZMPerson: 0x6000030f40e0> - 0x6000030f40e0 - 0x7ffeed054058
<ZMPerson: 0x6000030f40f0> - 0x6000030f40f0 - 0x7ffeed054050

从打印结果我们可以看到:

  • p1、p2、p3的对象地址是一模一样,但指针地址不一样并且是从高位到低位连续的间隔8字节。
  • newP和p1、p2、p3的对象地址,指针的地址是不一样的。

为什么呢:

  • alloc具备在堆中开辟内存的功能
  • init不具备在堆中开辟内存的功能,但可以在栈中生成指向堆内存的指针地址
  • 栈区开辟的内存是高地址低地址堆区则是低地址高地址

如图:

image.png

alloc探索

准备工作

  1. 下载objc4-818.2(版本号根据自己macOS版本变化)
  2. 编译源码

alloc流程图

image.png

四种探索方式

1. 断点模式 & control+step into

需要调试的alloc位置打上断点,编译走到断点的时候control+step into进入下一步的汇编里面,如图:
image.png
然后找到方法objc_alloc,并给这个方法添符号断点。如图:
image.png

image.png
然后继续编译,进到libobjc.A.dylib`objc_alloc,在这里我们看到objc_alloc里面有_objc_rootAllocWithZoneobjc_msgSend

image.png

2. 汇编(比较给手的方法,👏)

同样,在需要调试的alloc位置打上断点,然后Xcode->Debug->Debug Workflow -> Always Show Disassembly勾选Always Show Disassembly

image.png
然后找到方法objc_alloc直接添加符号断点调试,或者断住要跳转的地方,control+step into一步一步一直走

image.png

image.png

3. 符号断点,断位置

直接给要调试的方法添加符号断点,比如下图给我们探索的alloc添加符号断点,不过有个注意点,这个符号断点需要在 断住的方法 之后添加并激活,因为alloc项目运行中可能很多地方调用,那数据量,你说你不想知道结果,但那是考验机器性能和你识别度的。如图:

image.png
然后找到我们需要的alloc:
image.png

4. 编译源码LLDB调试

objc_alloc是属于libobjc库的方法,在alloc探索的准备模块里,我们准备好了objc源码,然后源码编译成工程run起来,这种就比较beautiful的方法
根据编译源码工程,我们点击进入alloc,更直观一目了然的看到alloc的执行过程,流程图如下:

进入alloc:

image.png

然后到_objc_rootAlloc:

image.png

然后到callAlloc:
  • 重磅提示 这里是核心方法
  • slowpath(x):x很可能为假,为真的概率很小
  • fastpath(x):x很可能为真
  • 其实将fastpath和slowpath去掉完全不影响任何功能,写上是告诉编译器对代码进行优化
    image.png
然后断点到_objc_rootAllocWithZone:

image.png
问题来了(这两个问题将在下面的 alloc init & new去解答):

  • 问题1,我们通过上面汇编发现是进到objc_alloc,为什么通过源码LLDB却走了_objc_rootAlloc
  • 问题2,我们打印alloc时在走到_objc_rootAlloc时,线程里面有了objc_alloc->callAlloc,然后走到NSObject的alloc -> _objc_rootAlloc -> callAlloc-> _objc_rootAllocWithZone及之后的流程,为什么?
然后断点进入核心方法_class_createInstanceFromZone:
  1. cls->instanceSize计算需要开辟的内存大小
  2. (id)calloc(1, size)向内存申请开辟空间,并返回地址指针
  3. obj->initInstanceIsa初始化的isa指针和cls关联
    image.png
核心方法-instanceSize

image.png
进入cache.fastInstanceSize
image.png

断点进入align16

  • 16字节对齐算法 公式:(x + 15)& ~15
    image.png

比如我们以align(9)为例:
image.png

总结:align16 算法实际上就是取16的整数倍。对于变量x来说是向上取整,比如30,向上取整为32(16<30 但是 16*2>30),算法的角度有人认为是向下取整:(x + 15)16几倍,超过的部分抹去。例如 (20 + 15) = 35 = 16 * 2 + 3,结果是32。这种算法和 >> 4 << 4 是一样的,得出的结果就是16的倍数,不足16的全部抹去。

为什么对象需要16字节对齐

  • cpu的性能和读取速度 cpu 读取数据是一个用空间换取时间的做法,以固定字节块来读取的,如果频繁的读取字节未对齐的数据,降低了cpu的性能和读取速度。
  • 更安全 由于在一个对象中isa指针是占8个字节,如果不进行节对齐 ,对象之间就会紧挨着,容易造成访问混乱。16字节对齐,会预留部分空间,访问更安全
核心方法-calloc
  • 开辟内存,返回地址指针
    instanceSize 方法 计算出需要的内存大小,然后向内存申请 size 大小的内存,返回给objc,因此objc是指向内存地址的指针
    image.png
    执行calloc后打印的是一个16进制的指针地址,说明已经开辟了内存
核心方法-initInstanceIsa

方法内部isa结构后续探索待补充
image.png
执行完initInstanceIsa后

image.png
图示打印结果说明:指针已经与类进行了关联

init探索

image.png

断点进入_objc_rootInit
image.png
可以看到init返回的是对象本身,可以提供给开发者更多的自由去自定义重写init ,通过id实现强转,返回我们需要的类型

new探索

image.png
new的流程走了callAlloc的方法流程,然后走了init方法 ,所以 new看做是alloc + init

alloc init & new

Calls [[cls alloc] init]
image.png

object_alloc问题及解答:

  • 如果直接LLDB源码调试NSObject*obj = [[NSObject alloc] init];能走进底层alloc么?为什么?
    如图:

image.png

image.png

image.png
经过源码一步一步探索我们发现并不能走到底层alloc代码但走了obj_alloc,但为什么呢?设置符号断点继续跟源码:

image.png

image.png
通过跟源码,我们发现子类进入这里经过了俩次,从alloc中进入,又进了object_alloc进行发送系统消息,然后又出来重新进入alloc方法,而NSObject却不是,他没有经过给系统发送消息的一步,直接开辟内存,所以我们可以猜测,它是在系统编译之后就已经执行了,跟程序员无关,那么问题又来了,为什么子类却要经过俩次呢?
解答:通过_read_images找到fixupMessageRef函数:

image.png
答案就出来了,在read_images做了一次fixup的修复,😂
然后我们通过LLVM源码:搜索objc_alloc

image.png

image.png

image.png
通过对LLVM探索:

  • alloc在系统被系统拦截 -> objc_alloc -> 标记receiver 判断进不去 这个时候objc_alloc-> objc_msgSend -> 然后重新走alloc方法这个时候receiver已经有了标记最后就正常走alloc流程(alloc-> _objc_rootAlloc -> …)
    如图:

image.png

Calls [cls new]
image.png
iOS开发中, alloc init <=> new

终章

alloc 的核心作用就是开辟内存,伴随初始化了isa,通过isa指针与类进行关联,init方法 工厂设计,子类可以自定义重写,提供开发者更多的自由,new 是对(alloc+init)进行了封装,无法在初始化的时候添加其它的需求

进阶

  • instanceSize & mallocSize 对象占用8字节对齐,系统分配内存16字节对齐为什么?
  • 编译器优化设置路径 Xcode -> target -> build Setting -> optimization -> Fast smallest
  • 编译器优化的作用:编译时间,链接时间,运行时间,空闲时间
  • isa怎么初始化的?怎么计算内存的?做了什么?对象的本质是什么?请看iOS底层-OC对象原理(下)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

☆MOON

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值