前言
解决的crash越多,越觉得了解对象的完整生命周期是一件很有必要的事情。所以有了这个系列,至于为什么先说dealloc,那是因为对象在创建的时候一般不会有问题,但是对象在释放的时候往往会有很多问题。
dealloc历史演进
MRC时代的dealloc
在MRC时代,我们需要手动管理对象的释放。例如:
1 | - (void)dealloc { |
简单来说就是先释放自己内部的成员变量和非Objc对象,然后再调用[super dealloc]
,继续父类的析构。
ARC时代的dealloc
但是到了ARC时代,代码就变成了:
1 | - (void)dealloc { |
可以看到相较于MRC,少了两部分:1. 释放内部成员变量(ivars) 2. 调用[super dealloc]
。
这是为什么呢,我们看一下ARC官网文档中的描述:
A class may provide a method definition for an instance method named dealloc. This method will be called after the final release of the object but before it is deallocated or any of its instance variables are destroyed. The superclass’s implementation of dealloc will be called automatically when the method returns.
大意是:一个类中可能会有一个默认的dealloc
实例方法,这个方法在实例和成员变量(ivars)释放之前被调用。当这个方法返回时会自动调用父类的dealloc
。这里我们就知道了[super dealloc]
是由系统自动调用的。
The instance variables for an ARC-compiled class will be destroyed at some point after control enters the dealloc method for the root class of the class. The ordering of the destruction of instance variables is unspecified, both within a single class and between subclasses and superclasses.
大意是:成员变量(ivars)会在root class(一般为NSObject
)的dealloc
方法中销毁。成员变量的释放顺序是不确定的。这回我们也知道了成员变量是在root class的dealloc
方法中销毁的,不需要我们手动去销毁。
dealloc实现
dealloc调用栈
既然dealloc的时候系统帮我们做了这么多事情,那具体是怎么实现的呢,就要看一下runtime的源码了。从网上找到runtime 723版本的源码,我们在NSObject.mm
中找到了dealloc
方法。
1 | - (void)dealloc { |
我们可以看到dealloc
只是简单的调用了_objc_rootDealloc
,_objc_rootDealloc
又调用了rootDealloc
。一路跟踪下去,rootDealloc
调用object_dispose
,最后调用objc_destructInstance
。详细来看objc_destructInstance
:
1 | void *objc_destructInstance(id obj) |
可以看到代码按照顺序执行了三步操作:
- 如果
hasCxxDtor
,则执行object_cxxDestruct
,调用C++的析构函数,清理ARC成员变量。 - 如果
hasAssociatedObjects
,则执行_object_remove_assocations
,字面意思移除assocate的对象。 - 最后执行
clearDeallocating
,清空引用计数表并清除弱引用表,将所有weak引用指nil。这回我们知道weak引用是什么时候被置为nil了。
object_cxxDestruct
一个一个来,先来看object_cxxDestruct
:
1 | void object_cxxDestruct(id obj) |
object_cxxDestruct
又调用了object_cxxDestructFromClass
,这里面主要做了一件事,那就是递归查找父类中名为SEL_cxx_destruct
的方法并调用。那么SEL_cxx_destruct
是啥呢,可惜的是在runtime源码中没有找到具体的定义,只知道这是一个全局的SEL
变量。
_object_remove_assocations
1 | void _object_remove_assocations(id object) { |
做的事情大致上就是找到对象的所有关联对象,然后放到一个数组中,接着遍历整个数组释放每个对象。
clearDeallocating
1 | inline void |
这里主要是判断一下isa.nonpointer
,nonpointer
这个属性主要是arm64架构引入的一个概念,对我们理解流程没有大的影响,两个分支最后都会调用到weak_clear_no_lock
方法里面,这个代码比较长就不贴了,主要做的就是从weaktable中找到对应的引用,将引用置为nil,再从weaktable中移除。感兴趣的可以自己看下源码。
隐藏的.cxx_destruct
现在就只剩下SEL_cxx_destruct
到底做了什么不清楚了,经过搜索在runtime源码的注释中找到一些线索:
1 | // object may have -.cxx_destruct implementation? |
在hasCxxDtor
方法的注释中,我们可以看到.cxx_destruct
这样一个方法。然后上网搜了一些应该是这个方法没错了。
《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.
也就是说编译器在看到一个对象中有C++对象时,就会生成一个.cxx_destruct
的方法用于析构,ARC借用这个方法插入了清理成员变量的操作。
为了进一步的验证,我们用Xcode断点来看一下实际情况,测试代码如下:
1 | @interface Fruit : NSObject |
在end
出设置断点,观察name成员变量:watchpoint set variable fruit->_name
然后继续断点,会发现name
从0x0000000105524088
被修改为0x0000000000000000
,即nil
。此时观察调用栈:
可以看到[Fruit .cxx_destruct]
的调用,说明object_cxxDestructFromClass
之后确实是调用了.cxx_destruct
,并且是在.cxx_destruct
中释放的成员变量。
深入clang
可惜的是.cxx_destruct
的具体实现并不在runtime的源码中,不过通过查找相关资料,发现.cxx_destruct
的实现是在clang前端编译时完成的,好在clang也是开源的,那么就好说了。
1 | void CodeGenModule::EmitObjCIvarInitializations(ObjCImplementationDecl *D) { |
首先判断needsDestructMethod
,这个方法就是遍历类的所有成员变量(ivars),如果变量是需要析构的,那么就会返回true
。接下来就是创建了一个名为.cxx_destruct
的方法,把这个实例方法加入到这个类中,并将HasDestructors
设置为true
。而.cxx_destruct
的具体实现就是由GenerateObjCCtorDtorMethod
这个方法生成的。继续看GenerateObjCCtorDtorMethod
发现只是简单的判断了一下,然后就调用到了emitCXXDestructMethod
。
1 | static void emitCXXDestructMethod(CodeGenFunction &CGF, |
这里主要做的就是遍历当前对象中的所有成员变量(ivars),如果是需要析构的且是__strong
类型的,则调用objc_storeStrong
去释放这个变量。其他的则调用默认的析构函数去释放。可惜的是没有找到objc_storeStrong
的源代码,只是找到了官方定义的伪代码:
1 | void objc_storeStrong(id *object, id value) { |
基本就是先赋值再释放的过程,这里我们传入的参数object就是要释放的对象,value就是nil。也就是说对象先被置为nil,再释放了原有的值。
其实objc_storeStrong这个方法是对象赋值时用到的操作,这里用来释放对象是一个比较trick的操作。不过我们从这里就可以知道多线程操作读写同一个对象为什么是不安全的,因为当线程A在赋值的时候,线程B可能正好拿到了对象的oldValue,因此线程B后面在操作oldValue的时候其实已经被释放了,导致crash。
总结
- 对象销毁的时候内部成员变量(ivars)的释放和
[super dealloc]
的调用都是由系统完成的 - 内部成员变量(ivars)的释放是通过在编译阶段插入的
.cxx_destruct
方法来完成的 - 关联对象清除和weak引用置为nil也是在dealloc的时候做的
- 三个析构方法的执行顺序为什么是这样的?后面可以再深入想想
参考
- http://clang.llvm.org/docs/AutomaticReferenceCounting.html#dealloc
- http://blog.sunnyxx.com/2014/04/02/objc_dig_arc_dealloc/
- https://www.jianshu.com/p/a8eade8a1c6d
- http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
- https://github.com/RetVal/objc-runtime
- http://blog.tracyone.com/2015/06/15/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAARC-%E4%B8%AD/
- http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/