写在前面
一、理解 alloc & init & new
首先说下三个方法做了什么:
alloc
:申请内存,创建对象init
:什么都没做,返回对象本身new
:相当于alloc
+init
然后看下面的代码:
打印结果为:
由打印结果,可以看出,p1,p2对象的地址是一样的,和p3不一样。他们的指针地址都不一样。而且p1,p2,p3是一串连续的间隔8字节的指针地址,地址从高到低的顺序排列。
由此总结如下:
- p1,p2指向了同一个对象,这个对象是
<SSPerson:0x600002abd0a0>
- 三个指针的地址是
连续的存储在栈区
中,并且从高位向低位开辟内存空间
3. 指针
占用空间是8个字节
4. p1
通过alloc方法
开辟了内存空间,创建了对象,通过init方法
初始化的p2
并没有开辟内存空间,也就是没有创建对象。p3
通过new
也开辟了内存空间,创建了对象。
二、源码调试alloc流程
打开已经配置好的源码工程,断点一步步跟流程:
__OBJC2__
用来判断objc的版本是不是objc2,当前我们使用的都是新版本objc2.0slowpath
告诉编译器这里大概率为假,fastpath
告诉编译器这里大概率为真,用来支持编辑器对代码进行优化fastpath
中的cls->ISA()->hasCustomAWZ()
由此判断会有两个执行分支,这里通过断点调试,发现callAlloc
方法走了两次,具体原因请看下文第四部分分析hasCustomAWZ
。接着断点执行到if
里面的代码,即走到_objc_rootAllocWithZone
通过以上断点跟踪,可得到allooc
的流程图:
三、alloc核心方法分析
_class_createInstanceFromZone
这个方法就是alloc
的核心流程了。
1. cls->instanceSize
此流程会计算出需要的内存空间⼤⼩。跟综源码流程进入到instanceSize
方法
进行了编译器优化
,更容易执行缓存中fastInstanceSize方法
,进行快速计算所需的内存空间大小
。
最终会进入align16方法
,这个方法是16字节对齐
算法。
关于字节对齐,后续会细讲。
2. calloc
向系统申请开辟内存,返回地址指针。此流程会临时分配一个脏内存,调用calloc
后分配的内存空间才是创建对象的内存地址。
3. obj->initInstanceIsa
关联到相应的类,即将开辟的内存空间指向所要关联的类!
通过运行结果发现,在调用obj->initInstanceIsa
之前,obj
只有一个内存地址,而调用之后明确了对象类型为SSPerson
。
以上,得出alloc核心流程图:
四、两个问题
1.分析objc_alloc
先看如下代码:
断点走到[NSObject alloc]
时,我们通过按住control + step into
汇编跟踪alloc
方法,发现调用的是objc_alloc
方法。
于是,我在两个方法处打上断点,跟踪到底走的哪个方法。
验证后发现,调用的alloc方法
,结果运行进入了objc_alloc方法
中。
产生疑问: alloc方法
和objc_alloc方法
,是怎么做到的呢?难道是运行时的方法替换?
于是验证:
- 首先全局搜索
objc_alloc
,找到了fixupMessageRef
方法,打断点并运行,发现未走进断点,fixupMessageRef
的调用向上追溯,发现是在_read_images
中进行了调用,它的作用是对objc_msgSend
进行修复,但这个方法也没有执行,alloc
的imp
指向却变了,而在源码中我们没有再找到其他的修改alloc imp
的地方。 - 因此不是运行时修改的,那会不会是在编译阶段就进行了替换呢,打开llvm,全局搜索
objc_alloc
,在tryGenerateSpecializedMessageSend
中找到了我们想要的结果,进入到EmitObjCAlloc
,
- 如图就是在这里
llvm
将alloc
的imp
修改到了objc_alloc
2.分析hasCustomAWZ
由callAlloc
方法知,在hasCustomAWZ()
处流程出现了分支。于是点进去查看源码:
其中,FAST_CACHE_HAS_DEFAULT_AWZ
是一个宏定义
进入getBit:
flags是cache_t的一个成员,用来以二进制的方式记录一些信息,将_flags
和FAST_CACHE_HAS_DEFAULT_AWZ
做与操作
进行验证是否实现了alloc/allocWithZone
。
分析:能否进入_objc_rootAllocWithZone
则是由_flags来决定,断点调试发现,SSPerson第一次不能直接进入,(走的是objc_msgSend
分支),而第二次调用却可以,那说明_flags的AWZ标记位在SSPerson第一次调用后被置为了1。同时发现,NSObject第一次调用就可以进入,说明NSObject默认AWZ标记位1。
接下来断点调试源码验证下:
- 全局搜索
FAST_CACHE_HAS_DEFAULT_AWZ
发现只在三个地方出现,其中包含一个get和两个set方法,hasCustomAWZ方法运行过之后在set方法打上断点,发现会进入到setHasDefaultAWZ()
查看堆栈信息:
由堆栈信息,我们进入到objc_class::setInitialized()
,从注释可以看出,NSObject
默认就会被标记为AWZ
,因此NSObject
第一次就可以进入到_objc_rootAllocWithZone
中。
查看整个调用栈,发现lookUpImpOrForward()
-_objc_msgSend_uncached
,验证了[SSPerson alloc]
第一次先执行objc_msgSend
分支流程,是在通过objc_msgSend
调用alloc
。
由上文的源码调试alloc流程知,alloc
->_objc_rootAlloc
->callAlloc
。因此会第二次进入到callAlloc方法。
到了这里思路基本就清晰了。
总结如下
[SSPerson alloc]
第一次if (fastpath(!cls->ISA()->hasCustomAWZ()))
为假,进入objc_msgSend
方法,进行消息查找。alloc/allocWithZone
在NSObject已经实现,所以消息查找一定可以找到并存到cache中。- 经过一系列的方法调用到了
setHasDefaultAWZ()
,将AWZ标记改为1- 第二次调用时已经标记了AWZ,所以
if (fastpath(!cls->ISA()->hasCustomAWZ()))
为真,直接进入到_objc_rootAllocWithZone
中。[NSObject alloc]
默认标记AWZ为1,直接进入_objc_rootAllocWithZone
。
五、alloc流程图完整版
六、init探索
查看源码:
init
方法返回的是对象本身init
方法是一个构造方法,给开发者提供构造方法入口,是工厂模式的一种运用,通过id
实现强转,返回我们需要的类型
七、new探索
查看源码:
由源码知,调用的是callAlloc
方法流程,然后是init
方法。
new
相当于alloc
+init
建议:开发中最好不常使用new,因为重写init方法比较常见,例如initWithxxx,如果使用了new,就无法调用自定义的init方法了。