Objc 对象的今生今世

111194012-8b5a727f7cf445c7

前言

在面向对象编程中,我们每天都在创建对象,用对象描述着整个世界,然而对象是如何从孕育到销毁的呢?

目录

  • 1.孕育对象
  • 2.对象的出生
  • 3.对象的成长
  • 4.对象的销毁
  • 5.总结
一.孕育对象
 121194012-82c9e8f42c5ba4de

每天开发我们都在alloc对象,而alloc方法做了些什么呢?

所有对象alloc都会调用这个root的方法

这个方法又会去调用callAlloc方法

由于入参 checkNil = false,所以不会返回nil。

131194012-46d3dd7df1c54def

这张图,我们可以看到在对象的数据段data中,class_rw_t中有一个flags。

RW_HAS_DEFAULT_AWZ 这个是用来标示当前的class或者是superclass是否有默认的alloc/allocWithZone:。值得注意的是,这个值会存储在metaclass 中。

hasDefaultAWZ( )方法是用来判断当前class是否有默认的allocWithZone。

如果cls->ISA()->hasCustomAWZ()返回YES,意味着有默认的allocWithZone方法,那么就直接对class进行allocWithZone,申请内存空间。

allocWithZone会去调用rootAllocWithZone

接下来就仔细看看_objc_rootAllocWithZone的具体实现

在__OBJC2__中,直接调用class_createInstance(cls, 0);方法去创建对象。

关于_class_createInstanceFromZone方法这里先不详细分析,下面再详细分析,先理清程序脉络。

在objc的老版本中要先去看看zone是否有空间,是否用了垃圾回收,如果没有空间,或者用了垃圾回收,就会调用class_createInstance(cls, 0)方法获取对象,否则调用class_createInstanceFromZone(cls, 0, zone);获取对象。

可以看到,创建对象最终调用的函数都是_class_createInstanceFromZone,不管objc的版本是新版还是旧版。

如果创建成功就返回objc,如果创建失败,就会调用callBadAllocHandler方法。

创建对象失败后,最终会调用_objc_fatal输出”attempt to allocate object of class failed”创建对象失败。

到此就完成了callAlloc中hasCustomAWZ( )返回YES的情况。那么hasCustomAWZ( )函数返回NO,情况是怎么样的呢?

这一段是hasCustomAWZ( )返回NO的情况,对应的是当前class没有默认的allocWithZone的情况。

在没有默认的allocWithZone的情况下,还需要再次判断当前的class是否支持快速alloc。如果可以,直接调用calloc函数,申请1块bits.fastInstanceSize()大小的内存空间,如果创建失败,也会调用callBadAllocHandler函数。

如果创建成功,就去初始化Isa指针和dtor。

dtor是用来判断当前class或者superclass是否有.cxx_destruct函数的实现。

如果当前的class不支持快速alloc,那么就乖乖的去调用class_createInstance(cls, 0);方法去创建一个新的对象。

小结一下:

141194012-50de17e68fd738c7

经过上面的一系列判断,“孕育对象”的过程最终落在了_class_createInstanceFromZone函数上了。

ctor 和 dtor 分别是什么呢?

ctor是判断当前class或者superclass 是否有.cxx_construct构造方法的实现。

dtor是判断判断当前class或者superclass 是否有.cxx_destruct析构方法的实现。

实例大小 instanceSize会存储在类的 isa_t结构体中,然后经过对齐最后返回。

注意:Core Foundation 需要所有的对象的大小都必须大于或等于 16 字节。

在获取对象大小之后,直接调用calloc函数就可以为对象分配内存空间了。

关于calloc函数

The calloc( ) function contiguously allocates enough space for count objects that are size bytes of memory each and returns a pointer to the allocated memory. The allocated memory is filled with bytes of value zero.

这个函数也是为什么我们申请出来的对象,初始值是0或者nil的原因。因为这个calloc( )函数会默认的把申请出来的空间初始化为0或者nil。

申请完内存空间之后,还需要再初始化Isa指针。

初始化Isa指针有这上面两个函数。

从上述源码中,我们也能看出,最终都是调用了initIsa函数,只不过入参不同。

初始化的过程就是对isa_t结构体初始化的过程。

具体初始化的过程请参见这篇神经病院Objective-C Runtime入院第一天——isa和Class

将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0
绝大多数机器的架构都是 byte-addressable 的,但是对象的内存地址必须对齐到字节的倍数,这样可以提高代码运行的性能,在 iPhone5s 中虚拟地址为 33 位,所以用于对齐的最后三位比特为 000,我们只会用其中的 30 位来表示对象的地址。

至此,孕育对象的过程就完成了。

二.对象的出生

151194012-be004f64f4d5e19c

一旦当我们调用init方法的时候,对象就会“出生”了。

init会调用_objc_rootInit方法。

而_objc_rootInit方法的作用也仅仅就是返回了当前对象而已。

三.对象的生长

161194012-8f7e962869251062

关于对象的生长,其实是想谈谈对象初始化之后,访问它的属性和方法,它们在内存中的样子。

这里我们新建一个Student类,来举例说明。这个类很简单,只有一个name的属性,加上一个类方法,和一个实例方法。

写出上述的代码,分析一下结构。

输出如下:

经过上面的打印结果,我们可以知道,一个类的实例的isa是指向它的class,如下图:

171194012-af06cbc665c6140d

一个类的实例,虚线指向灰色的区域,灰色的区域是一个Class pair,里面包含两个东西,一个是类,另一个是meta-class。类的isa指向meta-class。由于student是继承NSObject,所以Student的class的meta-class的superclass是NSObject。

为了弄清楚这3个东西里面分别存了些什么,我们进一步的打印一些信息。

从之前的打印信息我们能知道,0x100004d90是类的地址。0x100004d68是meta-class类的地址。

打印出来:

从这里就知道了,属性这些是存储在类中。

接下来就是关于类方法和实例方法的认识,+号方法和-号方法的认识。

在内存中其实没有+号和-号方法的概念。做个试验:

打印出来:

0x100004d90是类对象,里面存储的是-号方法,还有另外3个方法,getter,setter,还有.cxx_destruct方法

0x100004d68是meta-class,里面存储的是+号方法。

当然在runtime的meta-class有一处很特殊,那就是NSObject的meta-class,它的superclass是它自己本身。为了防止调用NSObject协议里面的减号方法可能会出现崩溃,比如copy的-号方法,于是在NSObject的meta-class里面把所有的NSObject的+号方法都重新实现了一遍,就是为了消息传递到这里,拦截了一遍。所以一般NSObject协议方法同一个方法都有+号和-号方法。

值得说明的是,class和meta-class都是单例。

关于对象,所有的对象在内存里面都有一个isa,isa就是一个小“雷达”,有了它,就可以在runtime下给一个对象发送消息了。

所以对象的实质:Objc中的对象是一个指向ClassObject地址的变量,即 id obj = &ClassObject 。

关于对象的属性实质是,void *ivar = &obj + offset(N)

输出

从这个例子就可以说明,对象的实质就是指向类对象的地址变量,从上面例子里面obj就可以看出, id obj = &ClassObject ,cls是Student的类对象,所以obj是Student的对象。

类对象是在main函数执行之前就加载进内存的,可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理,再这之后,runtime 的那些方法(动态添加 Class、swizzle 等等才能生效)

具体可以看这篇文章iOS 程序 main 函数之前发生了什么

还是回到例子中来,关于对象的属性,就是obj的地址加上偏移量,就可以访问到,上述的例子中,obj地址是0x7fff562eea98,往下偏移8,到了class的地址,0x7fff562eeaa0,再往下偏移8,就到了name属性的地址,0x7fff562eeaa8。在name中存储的是字符串的首地址,根据打印信息也看到了,存储的是一个指针,指向的0x10a25c068的地址。

如果我们打印一下这个地址:

181194012-fcf847ef2a23161c

就会发现里面存的就是我们的字符串。

191194012-44e2d8f0236e8fcf

总结一下就是上面这张图,每个对象的isa都存的是Class的内存地址,Class是在main函数执行之前就加载进内存的,并且由Runtime所管理。所以只需要构造一个指向Class的指针,即isa,就可以成为一个对象。

而对象的属性,就是在对象的首地址上进行的偏移。如上图,当知道对象的首地址是0x7fff562eea98,那么偏移8个字节就到了isa,再偏移8个字节就到了name属性了。对象的属性就是在内存中偏移寻址取值的过程。

四.对象的销毁

201194012-8a4e67277d28c3c2

对象的销毁就是调用dealloc方法。

dealloc方法会调用_objc_rootDealloc方法

如果是TaggedPointer,直接return。

indexed是代表是否开启isa指针优化。weakly_referenced代表对象被指向或者曾经指向一个 ARC 的弱变量。has_assoc代表对象含有或者曾经含有关联引用。has_cxx_dtor之前提到过了,是析构器。has_sidetable_rc判断该对象的引用计数是否过大。

object_dispose会调用objc_destructInstance。

销毁一个对象,靠的是底层的C++析构函数完成的。还需要移除associative的引用。

接下来就依次详细看看销毁对象的3个方法。

1.object_cxxDestruct

从子类开始沿着继承链一直找到父类,向上搜寻SEL_cxx_destruct
这个selector,找到函数实现(void (*)(id)(函数指针)并执行。

以下引用ARC下dealloc过程及.cxx_destruct的探究的内容:

这篇文章中:

ARC actually creates a -.cxx_destruct method to handle freeing instance variables. This method was originally created for calling C++ destructors automatically when an object was destroyed.

和《Effective Objective-C 2.0》中提到的:

When the compiler saw that an object contained C++ objects, it would generate a method called .cxx_destruct. ARC piggybacks on this method and emits the required cleanup code within it.

可以了解到,.cxx_destruct方法原本是为了C++对象析构的,ARC借用了这个方法插入代码实现了自动内存释放的工作。

在ARC中dealloc方法在最后一次release后被调用,但此时实例变量(Ivars)并未释放,父类的dealloc的方法将在子类dealloc方法返回后自动调用。ARC下对象的实例变量在根类[NSObject dealloc]中释放(通常root class都是NSObject),变量释放顺序各种不确定(一个类内的不确定,子类和父类间也不确定,也就是说不用care释放顺序)

经过@sunnyxx文中的研究:
1.ARC下对象的成员变量于编译器插入的.cxx_desctruct方法自动释放。
2.ARC下[super dealloc]方法也由编译器自动插入。

至于.cxx_destruct方法的实现,还请看@sunnyxx 那篇文章里面详细的分析。

2._object_remove_assocations

在移除关联对象object的时候,会先去判断object的isa_t中的第二位has_assoc的值,当object 存在并且object->hasAssociatedObjects( )值为1的时候,才会去调用_object_remove_assocations方法。

_object_remove_assocations方法的目的是删除第二张ObjcAssociationMap表,即删除所有的关联对象。删除第二张表,就需要在第一张AssociationsHashMap表中遍历查找。这里会把第二张ObjcAssociationMap表中所有的ObjcAssociation对象都存到一个数组elements里面,然后调用associations.erase( )删除第二张表。最后再遍历elements数组,把ObjcAssociation对象依次释放。

这里移除的方式和Associated Object关联对象里面的remove方法是完全一样的。

3.clearDeallocating( )

这里涉及到了2个clear函数,接下来一个个的看。

遍历SideTable,循环调用weak_clear_no_lock函数。

weakly_referenced代表对象被指向或者曾经指向一个 ARC 的弱变量。has_sidetable_rc判断该对象的引用计数是否过大。如果其中有一个为YES,则调用clearDeallocating_slow()方法。

clearDeallocating_slow也会最终调用weak_clear_no_lock方法。

这个函数会在weak_table中,清空引用计数表并清除弱引用表,将所有weak引用指nil。

总结

211194012-5f5835e699ffb512

这篇文章详细的分析了objc对象 从 出生 到 最终销毁,它的今生今世全部在此。还请大家多多指点。

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

1 1 收藏 评论

关于作者:一缕殇流化隐半边冰霜

已退役ACMer,现役iOS工程师。博观而约取,厚积而薄发。 个人主页 · 我的文章 · 5 ·   

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部