0. 前言
最近面了一些试,某位面试官问了我一个有意思的问题:
dealloc 的时候增加引用计数,会防止对象销毁吗?
当时猜测了一下,没答很全面,今晚有空了,好好梳理一下 delloc 的流程。
1. 调用Dealloc之前的流程
2. Dealloc()
2.1 析构 CxxDestruct,以及到底是什么!
2.2 移除关联对象,复习关联对象实现
3. clearDeallocationg(),处理weak
4. free(obj)
5. 父类的 Dealloc 呢?
穿插研究复习几个问题,提供几个真正属于 runtime 的面试题:
1. customRR是什么?
2. SideTable里操作Weak在什么环节?
3. CxxDestruct是什么?
4. 源码里没有,那父类Dealloc如何调用的?
5. 一个Strong一个weak在delloc里面指向对象自己会怎么样?
6. 为什么自己写 dealloc 里面不需要写 [super dealloc]?
1. 调用Dealloc之前的流程
首先创造一个简单对象的 delloc 流程下符号断点 delloc :
{
Fruit *banana = [[Fruit alloc] init];
}
可以捕获断点 bt
出来:
<Fruit: 0x100658430> (lldb) bt * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.2
frame #0: 0x000000010031e3f0 libobjc.A.dylib` -[NSObject dealloc](self=0x0000000100658430, _cmd="dealloc") at NSObject.mm:2350:23
frame #1: 0x00000001002c616b libobjc.A.dylib`_objc_release [inlined] objc_object::rootRelease(this=0x0000000100658430, performDealloc=true, =false) at objc-object.h:701:9
frame #2: 0x00000001002c5a98 libobjc.A.dylib`_objc_release [inlined] objc_object::rootRelease(this=0x0000000100658430) at objc-object.h:571
frame #3: 0x00000001002c5a98 libobjc.A.dylib`_objc_release [inlined] objc_object::release(this=0x0000000100658430) at objc-object.h:550
frame #4: 0x00000001002c5a09 libobjc.A.dylib`_objc_release(obj=0x0000000100658430) at NSObject.mm:1598
frame #5: 0x0000000100317bef libobjc.A.dylib`objc_storeStrong( location=0x00007ffeefbff4f0, obj=0x0000000000000000) at NSObject.mm:256:5
* frame #6: 0x0000000100000dfa `main(argc=1, argv=0x00007ffeefbff528) at main.m:17:9 [opt]
frame #7: 0x00007fff67a7ecc9 libdyld.dylib`start + 1
frame #8: 0x00007fff67a7ecc9 libdyld.dylib`start + 1
可以看到进入的流程:objc_storeStrong
--> _objc_release
--> release
......这就够了,最新的代码走起:
// PS: 如果不喜欢 bt 这个词的话,可以直接看:
// [Always Show Disassembly]
// 总之找到这个 dealloc 的入口是 objc_storeStrong() 就够了
-> 0x100001b5f <+31>: movq 0x173a(%rip), %rdi ; (void *)0x00000001000032e8: Fruit
0x100001b66 <+38>: callq 0x100001d4a ; symbol stub for: objc_alloc_init
0x100001b6b <+43>: movq %rax, -0x10(%rbp)
0x100001b6f <+47>: leaq -0x10(%rbp), %rdi
0x100001b73 <+51>: xorl %esi, %esi
0x100001b75 <+53>: callq 0x100001d6e ; symbol stub for: objc_storeStrong
调用了 NSObject.mm
的 objc_storeStrong()
方法,传入的参数是 对象的地址以及 obj 为 nil,其实就是一个赋空值的过程。
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
// 判断上一个值跟目前的值是否同一地址
if (obj == prev) {
return;
}
// 两个值,retain新值,release旧的值
objc_retain(obj);
*location = obj;
objc_release(prev);
}
很明显这个 objc_retain(nil);
没什么作用,重点就是 objc_release(banana);
objc_release() 方法的实现很简单,第一判空返回,第二判TaggedPointer返回,第三调用obj->release()
。
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
继续调用 objc_object::release()
// 等同于调用 [this release], 如果没有重写的话,就走一个捷径
inline void
objc_object::release()
{
ASSERT(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
rootRelease();
return;
}
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}
插入去研究了 什么是 customRR 和 customCore:
//class 或 superclass 实现了默认的 retain/release/autorelease/retainCount/ _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法
//class 或 superclass 有默认的 new/self/class/respondsToSelector/isKindOfClass 方法
//源码:
// class or superclass has default retain/release/autorelease/retainCount/
// _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define RW_HAS_DEFAULT_RR (1<<14)
// class or superclass has default new/self/class/respondsToSelector/isKindOfClass
#define RW_HAS_DEFAULT_CORE (1<<13)
也就是说,👆这里 fastpath 大多数情况会调用 rootRelease,如果你的类自己实现了RR方法,就给 msgSend release 方法。
继续跟踪 rootRelease()
ALWAYS_INLINE bool
objc_object::rootRelease()
{
return rootRelease(true, false);
}
rootRelease(true, false);
代码太多,看图里有详情,这里解释一下做的事情:
- 判 Tagged Pointer 返 false
- sideTable 和 extra_rc 的引用计数合并,如果都没有了,就通过 msgSend dealloc
另外这里提到:sidetable 使用的锁是自旋
问题1:Tagged Pointer 为啥这个时候返回False
TP不参与任何的引用计数,引用本身即是值,因为他很单纯,指针混淆、身体里大部分都是值。讲TP文章太多,在此不赘述了。
问题2:为什么会有 sideTable 和 extra_rc?
首先:nonpointerISA 对象的引用计数优先放在isa里面的ExtraRc里面,直到突破了那个区域的限制,就转到 SideTable 中的 两个Map中存储。
其次:而 pointerISA 直接在 SideTable 中的 两个Map中存储。
问题3:开篇的面试题
没有用,会继续释放,因为进入 dealloc 之前,问题2,已经判断了引用计数。
引发了另一个我想到的问题
一个Strong一个weak在delloc里面指向对象自己,会怎么样?稍后尝试!!!
我这里根据下面的推测猜测一下,strong 可能会有问题,甚至可能野指针,weak的话后面会释放,所以没关系。
2. Dealloc()
通过msgSend进入Dealloc
// NSObject.mm
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
// objc-object.h
inline void
objc_object::rootDealloc()
{
// fixme necessary? 源:有必要吗?
if (isTaggedPointer()) return;
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
好如果你这个对象:是nonpointter的话,请问你是否有弱引用
或者你是否有关联对象
或者是否有析构函数
或者是否你的引用计数太打了,存到sidetable里面去了
。
以上问题,如果你有一项的话,跟我们走一趟,调用下面的object_dispose((id)this)
如果以上你都没有,那好你是一个很单纯的对象,恭喜你,free()
and go!
另外,去执行 object_dispose((id)this)
的对象也不要担心,看下面的实现,只是在 free(obj)
之前多调用了一个 objc_destructInstance(obj)
而已。
/************************
* object_dispose
* fixme
* Locking: none
************************/
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
objc_destructInstance
这个是本篇的核心方法:
/************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
*************************************/
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
2.1 析构 CxxDestruct,以及到底是什么!
你看上面的源码说了,先执行析构函数还是先删除关联对象,先析构再移除关联。
object_cxxDestruct(obj)
往下走是 objc-class.mm 这个文件:两个方法,代码就不全粘了,核心就是一个for循环:
// 源:先调用 cls 的析构,再不断调用父类的析构函数
for ( ; cls; cls = cls->superclass) {
if (!cls->hasCxxDtor()) return;
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s",
cls->nameForLogging());
}
(*dtor)(obj);
}
}
看看这个for循环有意思,Cook多骚气,for ( ; cls; cls = cls->superclass)
,然后里面判断了hasCxxDtor
,没有C++Dtor的话人家不写break
,直接return
了。
如果这个for循环找到了C++析构,就调用lookupMethodInClassAndLoadCache,方法寻址,这个方法有注释,仅在构造和析构的时候使用,也做了断言,很安全。
内部实现是一个方法寻址,找到的话填充缓存返回。
关于这个C++的析构函数,这里详细说明下,这里详细释放了对象的每一个实例变量!所以这个方法一点是 LLVM 的 clang 中的 代码生成模块搞出来的。
http://clang.llvm.org/doxygen/CodeGenModule_8cpp-source.html
它的方法实现核心就是对于这个对象所有实例变量,遍历调用objc_storeStrong()
,然后这个实例变量就解除retain 了。
id objc_storeStrong(id *object, id value) {
value = [value retain];
id oldValue = *object;
*object = value;
[oldValue release];
return value;
}
2.2 移除关联对象,复习关联对象实现
看到这里你要知道,我们通过API添加的关联对象,不管你ARC还是MRC,都没必要手动Remove。
objc-references.mm
里面
// 与设置/获取关联的引用不同,
// 此函数对性能敏感,
// 因为原始的isa对象(例如OS对象)
// 无法跟踪它们是否具有关联的对象。
void
_object_remove_assocations(id object)
{
ObjectAssociationMap refs{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
refs.swap(i->second);
associations.erase(i);
}
}
// release everything (outside of the lock).
for (auto &i: refs) {
i.second.releaseHeldValue();
}
}
看删除就能看出来关联对象是如何管理的,这里复习一下:
- 有一个单例
AssociationsManager
- 通过
get()
获取了一个AssociationsHashMap
- 这个
AssociationsHashMap
里面 K:V 是disguise(obj)
:ObjectAssociation
-
ObjectAssociation
:结构体存储着:policy
,value
3. clearDeallocationg(),处理weak
继续前面的流程,完成2.1 2.2 之后 调用了 objc-object.h
里面的 clearDeallocating
方法:
// objc-object.h
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
最后这里才去 side table 里面去处理指针和弱引用问题。
注意 isa.nonpointer 分开两个逻辑。
通过这个对象取出 Side Table
,处理里面的 weakTable
,调用 weak_clear_no_lock(&table.weak_table, (id)this)
和 table.refcnts.erase(it)
完成清除。
将所有weak引用指nil,就是在这里实现的。
最后有一个debug的断言,专门写一个函数来做debug的断言,这个也是非常谨慎的体现,粘出来纪念一下Cook做饭的谨慎:
// NSObject.mm
#if DEBUG
//DEBUG 模式下才启用 :用于 断言 side table 中不存在对象。
bool
objc_object::sidetable_present()
{
bool result = false;
SideTable& table = SideTables()[this];
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) result = true;
if (weak_is_registered_no_lock(&table.weak_table, (id)this)) result = true;
table.unlock();
return result;
}
#endif
4. free(obj)
最后不管对象付不复杂,都会调用 free(obj)
Free 的实现在malloc的源码里面:
5. 父类的 Dealloc 呢?
至今看源码看不到任何调用父类dealloc 的地方,这个网上搜了一下,发现结果在这里!
http://clang.llvm.org/doxygen/CGObjC_8cpp_source.html
clang 在 ARC 的 dealloc 结束的时候插入了如雷的dealloc 的调用。
通过 forward 给 superclass 调用 dealloc
,才实现的[super dealloc]
操作。
这个时候上面有一个隐藏的问题也就解释了,其实 msgSend 来调用 dealloc 方法,会先调用 Fruit 的 dealloc (如果有的话),然后因为ClangCodeGen的插手,才有的调用父类一直到根类的 dealloc。
所有dealloc不需要我们手动写 super dealloc。
6 总结图
最后放上来一张 dealloc 图,好久好久之前画的。