生命之风的低语
Whispers in the Wind of Life.

dealloc是如何执行的

2025-08-07 05:20:54

前言:

本文将主要解答以下三个问题:weak 属性的为什么能自动置为nil、对象的实例变量是如何释放的、对象的关联对象释放的时机是什么?(这些答案的探究来源于其他同学的研究输出,本人只不过是站在前人的基础上,结合自身经验做一些加工输出)

ARC下的变化:

ARC下我们不需要再dealloc中主动调用[super dealloc],而且对象的实例变量会被释放掉。

对于经历过MRC开发的同学,会明显的产生以下疑惑:

1、[super dealloc]不需要手动,那是如何实现自动添加[super dealloc]的?

2、对象的实例变量是如何释放的?

3、weak属性为什么能自动置为nil?

4、加入一个对象存在关联对象,那他的关联对象是什么时间释放的?

下面我们一一解答:

明确结论:

1、dealloc的调用是在最后一次release执行后,但此时实例变量(ivars)并未释放。

2、父类的dealloc方法会在子类dealloc方法返回后自动执行。

3、ARC子类的实例变量在根类[NSObject dealloc]中释放。

NSObject的释放

通过runtime源码,很清晰的可以看,NSObject调用dealloc后产生函数调用链如下:

dealloc --> objc_rootDealloc -->objc_dispose -->objc_destructInstance

最终调用了一个objc_destructInstance函数,这个函数的定义如下:

void *objc_destructInstance(id obj) {

if (obj) {

Class isa_gen = _object_getClass(obj);

class_t *isa = newcls(isa_gen);

// Read all of the flags at once for performance.

bool cxx = hasCxxStructors(isa);

bool assoc = !UseGC && _class_instancesHaveAssociatedObjects(isa_gen);

//这里是重点

if (cxx) object_cxxDestruct(obj);

if (assoc) _object_remove_assocations(obj);

if (!UseGC) objc_clear_deallocating(obj);

}

return obj;

}

在objc_destructInstance函数中,我们可以看到这里面做了三件事情:

(1)object_cxxDestruct 做一些释放相关的操作

(2)_object_remove_assocations:移除对象的关联对象,也就是说对象的关联对象是在objc_destructInstance函数中释放的。(具体是如何执行关联对象的释放,后续我们还会讲到)

(3)objc_clear_deallocating:清空引用计数表和弱引用表,并将所有的weak引用置为nil。(也就是我们的weak引用在dealloc后能够自动置为nil是因为在这里执行了置为nil的操作)

既然我们清晰的看到这个函数就做了三件事,那对象的成员变量释放一定是在object_cxxDestruct中去做的了。

object_cxxDestruct这个方法的调用最终转化成了.cxx_destruct调用,而且实例变量的而释放是在.cxx_destruct调用的objc_storeStrong中释放的。(探究的过程会附上sunny的研究,感兴趣的同学可以自行实验)

ARC下对象实例变量的释放过程在.cxx_destruct内完成,但这个函数内部发生了什么,是如何调用objc_storeStrong释放变量的呢?

孙源在他的博客中给出了答案,具体规程看博客,这里只简化结论:

.cxx_destruct这个函数是编译器动态创建然后添加上去的,而且.cxx_destruct最终调用了emitCXXDestructMethod函数,这个函数遍历当前对象的所有实例变量,并调用objc_storeStrong函数。

objc_storeStrong在clang中的定义如下:

id objc_storeStrong(id *object, id value) {

value = [value retain];

id oldValue = *object;

*object = value;

[oldValue release];

return value;

}

可以看到,storeStrong中实例变量被release掉。

这里提一下两个验证方法:

1、NSObject+DLIntrospection

2、使用Watchpoint来观察内存的释放时机:

笔者通过 使用watchpoint捕获到如下调用栈,验证了在.cxxDestruct中最终调用objc_storeStrong来释放实例变量。

屏幕快照 2018-01-02 下午5.35.09.png

屏幕快照 2018-01-02 下午5.26.14.png

自动调用[super dealloc]

同样博客中提到在查阅clang代码时发现如下操作:

StartObjCMethod方法中:

if (ident->isStr("dealloc"))

EHStack.pushCleanup(getARCCleanupKind());

也就是dealloc在被调用时,编译器插入了一段代码FinishARCDealloc,继续跟进FinishARCDealloc实现会发现,函数实现的功能是向父类转发dealloc的调用,实现了自动调用[super dealloc]方法。

至此,我们就清楚了,为什么ARC下我们无需手动调用[super dealloc],因为编译器为我们做了这个操作,就想自动内存管理做作的一样,由编译器来为我们添加内存管理代码。

NSObject dealloc总结

1、ARC下对象的成员变量于编译器插入的.cxx_desctruct方法自动释放

2、ARC下[super dealloc]方法也由编译器自动插入

感谢sunny这篇博客有详细探索过程

关联对象:

针对关联对象我们有以下几点说明:

1、关联对象存在什么地方?

2、关联对象是如何存储?

3、对象销毁时候如何处理关联对象呢?

同样的,这些知识的研究离不开runtime源码,翻阅后你会发现设置设置对象的函数调用会转化成:_object_set_associative_reference,zai _object_set_associative_reference在runtime中的定义如下:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {

// retain the new value (if any) outside the lock.

ObjcAssociation old_association(0, nil);

id new_value = value ? acquireValue(value, policy) : nil;

{

AssociationsManager manager; //管理关联对象的manager

AssociationsHashMap &associations(manager.associations());

disguised_ptr_t disguised_object = DISGUISE(object);

if (new_value) {

// break any existing association.

AssociationsHashMap::iterator i = associations.find(disguised_object);

if (i != associations.end()) {

// secondary table exists

ObjectAssociationMap *refs = i->second;

ObjectAssociationMap::iterator j = refs->find(key);

if (j != refs->end()) {

old_association = j->second;

j->second = ObjcAssociation(policy, new_value);

} else {

(*refs)[key] = ObjcAssociation(policy, new_value);

}

} else {

// create the new association (first time).

ObjectAssociationMap *refs = new ObjectAssociationMap;

associations[disguised_object] = refs;

(*refs)[key] = ObjcAssociation(policy, new_value);

object->setHasAssociatedObjects();

}

} else {

// setting the association to nil breaks the association.

AssociationsHashMap::iterator i = associations.find(disguised_object);

if (i != associations.end()) {

ObjectAssociationMap *refs = i->second;

ObjectAssociationMap::iterator j = refs->find(key);

if (j != refs->end()) {

old_association = j->second;

refs->erase(j);

}

}

}

}

// release the old value (outside of the lock).

if (old_association.hasValue()) ReleaseValue()(old_association);

}

我们可以看到关联对象是由AssociationManager来管理的,同理我们看下AssociationManager定义:

class AssociationsManager {

static spinlock_t _lock;

static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap. 这行我们看到实际上它里面是维护了一个hashMap表。

public:

AssociationsManager() { _lock.lock(); }

~AssociationsManager() { _lock.unlock(); }

AssociationsHashMap &associations() {

if (_map == NULL)

_map = new AssociationsHashMap();

return *_map;

}

};

我们可以看到:AssociationsManager里面有一个静态的HashMap,以为是静态变量,所以存储在全局静态存储区,也就是这里的_map是一个全局的map,所有对象的关联对象是存储在一个全局的map中,key则是每个对象的内存地址object pointer。value又是另外一个AssociationsHashMap,里面包含了一个对象所有关联对象的kv对。

到这:我们的前两个问题就有结果了,关联对象是由AssociationsManager来管理,存储在AssociationsHashMap类型的全局表中。

load 和 initialize:

load函数声明:

load函数会在文件被加载时调用,因此它的调用一定是发生在main()执行前。

文档上如此描述:load函数会在类及其分类被添加到runtime时调用,实现这个函数可以在类加载时执行一些类相关的行为。

load父类以及分类中的调用顺序:

1、父类的load会早于子类load方法调用

2、所有本类加载完毕之后,再去加载分类的load方法

3、对于具体一个类的分类加载顺序:取决于Compile Source中的排列顺序。(我们手动调整后,执行顺序会发生相应变化)

继续深究可看源码

Initialize函数声明:

文档上对它的描述如下:

1、Initialize会在第一次给某个类发送消息时调用。

2、它是线程安全的所以不要写复杂的逻辑,防止造成死锁!!!

3、如果子类没有实现这个方法,父类的该方法会被调用多次,如果想防止Initialize被调用多次,可以使用下面的方法来避免:

+ (void)initialize {

if (self == [Parent class]) {

NSLog(@"Initialize Parent, caller Class %@", [self class]);

}

}

4、另外值得一提的一点是:它属于懒加载的方式,如果类或者子类在项目中没有被用到,则不会执行initialize函数。

initialize调用规则:

1、与load不同,父类中load的方法是由runtime主动调用,而这里是基于继承关系来调用,也就是在创建子类对象时,首先要创建父类对象,所以会调用一次父类的initialize方法。

2、子类未实现initialize,则会多次调用父类的该方法,上面对此已经提到。

3、分类中的initialize会覆盖原类中的initialize方法。

super 调用

+(void)initialize和+(void)load中,我们并不需要在这两个方法的实现中使用super调用父类的方法:

+ (void)initialize {

//do initialization thing

[super initialize];

}

+ (void) load {

//do some loading things

[super load];

}

super的方法会成功调用,但是这是多余的,因为runtime对自动对父类的+(void)load方法进行调用,而+(void)initialize则会随子类自动激发父类的方法(如Apple文档中所言)不需要显示调用。另一方面,如果父类中的方法用到的self(像示例中的方法),其指代的依然是类自身,而不是父类。

总结:

Tables

+(void)load

+(void)initialize

执行时机

在程序运行后立即执行

在类的方法第一次被调时执行

若自身未定义,是否沿用父类的方法?

类别中的定义

全都执行,但后于类中的方法

覆盖类中的方法,只执行一个

深入理解category:

https://tech.meituan.com/DiveIntoCategory.html

最后附上runtime源码如何编译查看,实际上查看源码并不是一个简单的过程,相信很多人只是单纯的去查看源码也不知从何下手:runtime源码编译教程