在分析alloc源码之前,先来看看一下3个变量 指针 和 内存地址 区别:
分别输出3个对象的内容
、指针地址
、对象地址
,下图是打印结果
结论:通过上图可以看出,3个对象指向的是同一个内存空间,所以其内容 和 指针地址是相同的,但是对象的内存地址是不同的
准备工作
- 下载 objc4-781 源码
这就是本文需要探索的内容,alloc做了什么?init做了什么?
alloc 源码探索
alloc + init 整体源码的探索流程如下
【第一步】首先根据main函数中的LGPerson类的alloc
方法进入alloc
方法的源码实现(即源码分析开始)
+ (id)alloc {
return _objc_rootAlloc(self);
}
【第二步】跳转至_objc_rootAlloc
的源码实现
【第三步】跳转至callAlloc
的源码实现
slowpath & fastpath
其中关于slowpath和fastpath这里需要简要说明下,这两个都是objc源码中定义的宏,其定义如下
//x很可能为真, fastpath 可以简称为 真值判断
#define fastpath(x) (__builtin_expect(bool(x), 1))
//x很可能为假,slowpath 可以简称为 假值判断
#define slowpath(x) (__builtin_expect(bool(x), 0))
其中的__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
【第四步】跳转至_objc_rootAllocWithZone
的源码实现
【第五步】跳转至_class_createInstanceFromZone
的源码实现,这部分是alloc
源码的核心操作,由下面的流程图及源码可知,该方法的实现主要分为三部分:
-
cls->instanceSize
:计算需要开辟的内存空间大小 -
calloc
:申请内存,返回地址指针 -
obj->initInstanceIsa
:将类
与isa
关联
alloc 核心操作
核心操作都位于calloc
方法中
cls->instanceSize:计算所需内存大小
- 内存字节对齐原则
在解释为什么需要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字节对齐
calloc:申请内存,返回地址指针
通过instanceSize
计算的内存大小,向内存中申请 大小 为 size的内存,并赋值给obj,因此 obj是指向内存地址的指针
obj->initInstanceIsa:类与isa关联
经过calloc可知,内存已经申请好了,类也已经传入进来了,接下来就需要将 类与 地址指针 即isa指针进行关联,其关联的流程图如下所示
主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行 关联
同样也可以通过断点调试来印证上面的说法,在执行完initInstanceIsa
后,在通过po obj
可以得出一个对象指针
总结
- 通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,而且开辟的内存需要使用16字节对齐算法,现在开辟的内存的大小基本上都是16的整数倍
- 开辟内存的核心步骤有3步:计算 -- 申请 -- 关联
init 源码探索
alloc源码探索完了,接下来探索init源码,通过源码可知,inti
的源码实现有以下两种:
-
类方法 init
+ (id)init {
return (id)self;
}
这里的init是一个构造方法
,是通过工厂设计
(工厂方法模式),主要是用于给用户提供构造方法入口
。这里能使用id强转的原因,主要还是因为 内存字节对齐后,可以使用类型强转为你所需的类型
-
实例方法 init
通过[[GLClass alloc]init]
代码进行探索实例方法 init- 通过main中的init跳转至init的源码实现
- 跳转至
_objc_rootInit
的源码实现
有上述代码可以,返回的是传入的self本身。
new 源码探索
一般在开发中,初始化除了init
,还可以使用new
,两者本质上并没有什么区别,以下是objc中new的源码实现,通过源码可以得知,new
函数中直接调用了callAlloc
函数(即alloc中分析的函数),且调用了init函数,所以可以得出new 其实就等价于 [alloc init]
的结论
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
但是一般开发中并不建议使用new
,主要是因为有时会重写init方法做一些自定义的操作,例如 initWithXXX
,会在这个方法中调用[super init]
,用new
初始化可能会无法走到自定义的initWithXXX
部分。
总结
如果子类没有重写父类的init,new会调用父类的init方法
如果子类重写了父类的init,new会调用子类重写的init方法
如果使用 alloc + 自定义的init,可以帮助我们自定义初始化操作,例如传入一些子类所需参数等,最终也会走到父类的init,相比new而言,扩展性更好,更灵活。