写在前面
一、理解 alloc & init & new
首先说下三个方法做了什么:
alloc:申请内存,创建对象init:什么都没做,返回对象本身new:相当于alloc+init
然后看下面的代码:
打印结果为:
由打印结果,可以看出,p1,p2对象的地址是一样的,和p3不一样。他们的指针地址都不一样。而且p1,p2,p3是一串连续的间隔8字节的指针地址,地址从高到低的顺序排列。
由此总结如下:
- p1,p2指向了同一个对象,这个对象是
<SSPerson:0x600002abd0a0> - 三个指针的地址是
连续的存储在栈区中,并且从高位向低位开辟内存空间 指针占用空间是8个字节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方法了。