前言:
本文将主要解答以下三个问题: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
来释放实例变量
。
自动调用[super dealloc]
同样博客中提到在查阅clang代码时发现如下操作:
StartObjCMethod方法中:
if (ident->isStr("dealloc"))
EHStack.pushCleanup<FinishARCDealloc>(getARCCleanupKind());
也就是dealloc在被调用时,编译器插入了一段代码FinishARCDealloc,继续跟进FinishARCDealloc实现会发现,函数实现的功能是向父类转发dealloc的调用,实现了自动调用[super dealloc]方法。
至此,我们就清楚了,为什么ARC下我们无需手动调用[super dealloc],因为编译器为我们做了这个操作,就想自动内存管理做作的一样,由编译器来为我们添加内存管理代码。
NSObject dealloc总结
1、ARC下对象的成员变量于编译器插入的.cxx_desctruct方法自动释放
2、ARC下[super dealloc]方法也由编译器自动插入
关联对象:
针对关联对象我们有以下几点说明:
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源码编译教程