注册

iOS-底层原理 02:alloc & init & new 源码分析

在分析alloc源码之前,先来看看一下3个变量 内存地址 和 指针地址 区别:

8fa0a3f8114916268860971edcb11e31.png

分别输出3个对象的内容、内存地址、指针地址,下图是打印结果

cce18c8064e30a9331a188ac20b23a2a.png

结论:通过上图可以看出,3个对象指向的是同一个内存空间,所以其内容 和 内存地址相同的,但是对象的指针地址是不同的

%p -> &p1:是对象的指针地址,
%p -> p1: 是对象指针指向的的内存地址

这就是本文需要探索的内容,alloc做了什么?init做了什么?

准备工作

alloc 源码探索

alloc + init 整体源码的探索流程如下

cde3cba177a603cd27697ff80d180bc3.png

  • 【第一步】首先根据main函数中的LGPerson类的alloc方法进入alloc方法的源码实现(即源码分析开始),
9637a49f58f746266a61c731acaf0ba7.png
  • 【第二步】跳转至_objc_rootAlloc的源码实现
ffd4308f25287843206fdbcd12828b90.png
  • 【第三步】跳转至callAlloc的源码实现6a90a9f78f4c07ed05629e18105d984a.png

如上所示,在calloc方法中,当我们无法确定实现走到哪步时,可以通过断点调试,判断执行走哪部分逻辑。这里是执行到_objc_rootAllocWithZone

slowpath & fastpath

其中关于slowpathfastpath这里需要简要说明下,这两个都是objc源码中定义的,其定义如下

964b79e54d8a658119c524c545306bfe.png

其中的__builtin_expect指令是由gcc引入的,
1、目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化
2、作用:允许程序员将最有可能执行的分支告诉编译器。
3、指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N的概率很大。
4、fastpath定义中__builtin_expect((x),1)表示 x 的值为真的可能性更大;即 执行if 里面语句的机会更大
5、slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大。即执行 else 里面语句的机会更大
6、在日常的开发中,也可以通过设置来优化编译器,达到性能优化的目的,设置的路径为:Build Setting --> Optimization Level --> Debug --> 将None 改为 fastest 或者 smallest

cls->ISA()->hasCustomAWZ()

其中fastpath中的 cls->ISA()->hasCustomAWZ() 表示判断一个类是否有自定义的 +allocWithZone 实现,这里通过断点调试,是没有自定义的实现,所以会执行到 if 里面的代码,即走到_objc_rootAllocWithZone


【第四步】跳转至_objc_rootAllocWithZone的源码实现

9e88e53d7be87bf9962766165163fd92.png

【第五步】跳转至_class_createInstanceFromZone的源码实现,这部分是alloc源码的核心操作,由下面的流程图及源码可知,该方法的实现主要分为三部分
cls->instanceSize:计算需要开辟的内存空间大小
calloc:申请内存,返回地址指针
obj->initInstanceIsa:将 类 与 isa 关联
c3d2096514b985914a1143f04a09024e.png

9b29f0643bf5aee6edae9ab260aabd6f.png

根据源码分析,得出其实现流程图如下所示:

2f28f2d931a4a075fa5d9a8b56480c07.png

alloc 核心操作

核心操作都位于calloc方法中

cls->instanceSize:计算所需内存大小

计算需要开辟内存的大小的执行流程如下所示

43800b48d2cc427d08ee2c1c9a82d971.png

  • 1、跳转至instanceSize的源码实现
86c0709d87f28ae935a0ed982ef87ad4.png

通过断点调试,会执行到cache.fastInstanceSize方法,快速计算内存大小

  • 2、跳转至fastInstanceSize的源码实现,通过断点调试,会执行到align16
715c496aff3f48ac97363877983ba2ea.png
  • 3、跳转至align16的源码实现,这个方法是16字节对齐算法
58a677dad936267135790194937371c6.png

内存字节对齐原则

在解释为什么需要16字节对齐之前,首先需要了解内存字节对齐的原则,主要有以下三点

数据成员对齐规则:struct 或者 union 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储)
数据成员为结构体:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
结构体的整体对齐规则:结构体的总大小,即sizeof的结果,必须是其内部做大成员的整数倍,不足的要补齐
为什么需要16字节对齐

需要字节对齐的原因,有以下几点:

通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数来降低cpu的开销
16字节对齐,是由于在一个对象中,第一个属性isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱
16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况
字节对齐-总结

在字节对齐算法中,对齐的主要是对象,而对象的本质则是一个 struct objc_object的结构体,
结构体在内存中是连续存放的,所以可以利用这点对结构体进行强转。
苹果早期是8字节对齐,现在是16字节对齐
下面以align(8) 为例,图解16字节对齐算法的计算过程,如下所示
02e93767a0fa9416879014ff989108cd.png

首先将原始的内存 8 与 size_t(15)相加,得到 8 + 15 = 23
将 size_t(15) 即 15进行~(取反)操作,~(取反)的规则是:1变为0,0变为1
最后将 23 与 15的取反结果 进行 &(与)操作,&(与)的规则是:都是1为1,反之为0,最后的结果为 16,即内存的大小是以16的倍数增加的

calloc:申请内存,返回地址指针

通过instanceSize计算的内存大小,向内存中申请 大小 为 size的内存,并赋值给obj,因此 obj是指向内存地址的指针

a1cf509f676eb32bb874a1c0f5f2c60a.png

这里我们可以通过断点来印证上述的说法,在未执行calloc时,po objnil,执行后,再po obj法线,返回了一个16进制的地址

5d9589c31d7dd3e36d1c4f74617d2c99.png

在平常的开发中,一般一个对象的打印的格式都是类似于这样的<LGPerson: 0x01111111f>(是一个指针)。为什么这里不是呢?

  • 主要是因为objc 地址 还没有与传入 的 cls进行关联,
  • 同时印证了 alloc的根本作用就是 开辟内存
obj->initInstanceIsa:类与isa关联

经过calloc可知,内存已经申请好了,类也已经出入进来了,接下来就需要将 类与 地址指针 即isa指针进行关联,其关联的流程图如下所示

311b1c032732b0ae7442c73db3f4de54.png

主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行 关联

同样也可以通过断点调试来印证上面的说法,在执行完initInstanceIsa后,在通过po obj可以得出一个对象指针

fd2253e80a81db7fcbcbe98f1f5a3fc1.png

总结

  • 通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,而且开辟的内存需要使用16字节对齐算法,现在开辟的内存的大小基本上都是16的整数倍
  • 开辟内存的核心步骤有3步:计算 -- 申请 -- 关联

init 源码探索

alloc源码探索完了,接下来探索init源码,通过源码可知,inti的源码实现有以下两种

类方法 init

79438c64ffeac8ff53c7b2171ab1f97a.png

这里的init是一个构造方法 ,是通过工厂设计(工厂方法模式),主要是用于给用户提供构造方法入口。这里能使用id强转的原因,主要还是因为 内存字节对齐后,可以使用类型强转为你所需的类型

实例方法 init

  • 通过以下代码进行探索实例方法 init
595b893244feac077e9a8d629610118f.png
  • 通过main中的init跳转至init的源码实现
07bee2b427de6ef7e9cf90f1cc890485.png
  • 跳转至_objc_rootInit的源码实现
f0c45c7a142274fe76a1c51782ac89fa.png
有上述代码可以,返回的是传入的self本身。

new 源码探索

一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实现,通过源码可以得知,new函数中直接调用了callAlloc函数(即alloc中分析的函数),且调用了init函数,所以可以得出new 其实就等价于 [alloc init]的结论

6f5aeaf382602cacc1163b927286a5cd.png

但是一般开发中并不建议使用new,主要是因为有时会重写init方法做一些自定义的操作,例如 initWithXXX,会在这个方法中调用[super init],用new初始化可能会无法走到自定义的initWithXXX部分。

例如,在CJLPerson中有两个初始化方法,一个是重写的父类的init,一个是自定义的initWithXXX方法,如下图所示

f4b9693fcc17d56bbe89d2f75ae74ced.png

使用 alloc + init 初始化时,打印的情况如下

438761da9823186a93ca6bc42a425a01.png

使用new 初始化时,打印的情况如下0ae861a767baa55eb532d23986de815a.png

总结

如果子类没有重写父类的init,new会调用父类的init方法
如果子类重写了父类的init,new会调用子类重写的init方法
如果使用 alloc + 自定义的init,可以帮助我们自定义初始化操作,例如传入一些子类所需参数等,最终也会走到父类的init,相比new而言,扩展性更好,更灵活。

补充

【问题】为什么无法断点到obj->initInstanceIsa(cls, hasCxxDtor);

主要是因为断点断住的不是 自定义类的流程,而是系统级别的

0cee700385a56832d45e6085a1e5f13e.png

作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108427260

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册