iOS底层原理02:alloc探索

写在前面

一、理解 alloc & init & new

首先说下三个方法做了什么:

  • alloc :申请内存,创建对象
  • init :什么都没做,返回对象本身
  • new :相当于alloc+init

然后看下面的代码:

image.png 打印结果为:

image.png 由打印结果,可以看出,p1,p2对象的地址是一样的,和p3不一样。他们的指针地址都不一样。而且p1,p2,p3是一串连续的间隔8字节的指针地址,地址从高到低的顺序排列。
由此总结如下:

  1. p1,p2指向了同一个对象,这个对象是<SSPerson:0x600002abd0a0>
  2. 三个指针的地址是连续的存储在栈区中,并且从高位向低位开辟内存空间

image.png 3. 指针占用空间是8个字节
4. p1通过alloc方法开辟了内存空间,创建了对象,通过init方法初始化的p2并没有开辟内存空间,也就是没有创建对象。p3通过new也开辟了内存空间,创建了对象。

二、源码调试alloc流程

打开已经配置好的源码工程,断点一步步跟流程:

image.png

image.png

image.png

image.png

  • __OBJC2__用来判断objc的版本是不是objc2,当前我们使用的都是新版本objc2.0
  • slowpath告诉编译器这里大概率为假,fastpath告诉编译器这里大概率为真,用来支持编辑器对代码进行优化
  • fastpath中的cls->ISA()->hasCustomAWZ()由此判断会有两个执行分支,这里通过断点调试,发现callAlloc方法走了两次,具体原因请看下文第四部分分析hasCustomAWZ。接着断点执行到if里面的代码,即走到_objc_rootAllocWithZone

image.png

image.png

通过以上断点跟踪,可得到allooc的流程图:

alloc流程图.png

三、alloc核心方法分析

_class_createInstanceFromZone这个方法就是alloc的核心流程了。

image.png

1. cls->instanceSize

此流程会计算出需要的内存空间⼤⼩。跟综源码流程进入到instanceSize方法

image.png

进行了编译器优化,更容易执行缓存中fastInstanceSize方法,进行快速计算所需的内存空间大小

image.png

image.png

最终会进入align16方法,这个方法是16字节对齐算法。 16字节算法.png
关于字节对齐,后续会细讲。

2. calloc

向系统申请开辟内存,返回地址指针。此流程会临时分配一个脏内存,调用calloc后分配的内存空间才是创建对象的内存地址。

image.png

3. obj->initInstanceIsa

关联到相应的类,即将开辟的内存空间指向所要关联的类!通过运行结果发现,在调用obj->initInstanceIsa之前,obj只有一个内存地址,而调用之后明确了对象类型为SSPerson

image.png 以上,得出alloc核心流程图: alloc核心流程.png

四、两个问题

1.分析objc_alloc

先看如下代码:

image.png 断点走到[NSObject alloc]时,我们通过按住control + step into汇编跟踪alloc方法,发现调用的是objc_alloc方法。
于是,我在两个方法处打上断点,跟踪到底走的哪个方法。

image.png image.png 验证后发现,调用的alloc方法,结果运行进入了objc_alloc方法中。

产生疑问: alloc方法objc_alloc方法,是怎么做到的呢?难道是运行时的方法替换?

于是验证:

  • 首先全局搜索objc_alloc,找到了fixupMessageRef方法,打断点并运行,发现未走进断点,fixupMessageRef的调用向上追溯,发现是在_read_images中进行了调用,它的作用是对objc_msgSend进行修复,但这个方法也没有执行,allocimp指向却变了,而在源码中我们没有再找到其他的修改alloc imp的地方。
  • 因此不是运行时修改的,那会不会是在编译阶段就进行了替换呢,打开llvm,全局搜索objc_alloc,在tryGenerateSpecializedMessageSend中找到了我们想要的结果,进入到EmitObjCAlloc

1.png

  • 如图就是在这里llvmallocimp修改到了objc_alloc

2.png

2.分析hasCustomAWZ

image.png

callAlloc方法知,在hasCustomAWZ()处流程出现了分支。于是点进去查看源码:

image.png

其中,FAST_CACHE_HAS_DEFAULT_AWZ是一个宏定义

image.png

进入getBit:

image.png

flags是cache_t的一个成员,用来以二进制的方式记录一些信息,将_flagsFAST_CACHE_HAS_DEFAULT_AWZ与操作进行验证是否实现了alloc/allocWithZone
分析:能否进入_objc_rootAllocWithZone则是由_flags来决定,断点调试发现,SSPerson第一次不能直接进入,(走的是objc_msgSend分支),而第二次调用却可以,那说明_flags的AWZ标记位在SSPerson第一次调用后被置为了1。同时发现,NSObject第一次调用就可以进入,说明NSObject默认AWZ标记位1。
接下来断点调试源码验证下:

  1. 全局搜索FAST_CACHE_HAS_DEFAULT_AWZ发现只在三个地方出现,其中包含一个get和两个set方法,hasCustomAWZ方法运行过之后在set方法打上断点,发现会进入到setHasDefaultAWZ()

image.png

查看堆栈信息:

image.png

由堆栈信息,我们进入到objc_class::setInitialized(),从注释可以看出,NSObject默认就会被标记为AWZ,因此NSObject第一次就可以进入到_objc_rootAllocWithZone中。

image.png

查看整个调用栈,发现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流程图完整版

alloc流程图.png

六、init探索

查看源码:

image.png

image.png

  • init方法返回的是对象本身
  • init方法是一个构造方法,给开发者提供构造方法入口,是工厂模式的一种运用,通过id实现强转,返回我们需要的类型

七、new探索

查看源码:

image.png

由源码知,调用的是callAlloc方法流程,然后是init方法。

  • new相当于alloc+init

建议:开发中最好不常使用new,因为重写init方法比较常见,例如initWithxxx,如果使用了new,就无法调用自定义的init方法了。

猜你喜欢

转载自juejin.im/post/6996899535465234446