dealloc是如何执行的

前言:

本文将主要解答以下三个问题: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源码编译教程

你可能感兴趣的:(dealloc是如何执行的)