面试遇到的 OC 的 dealloc() 问题思考

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 出来:

 (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.mmobjc_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); 代码太多,看图里有详情,这里解释一下做的事情:

  1. 判 Tagged Pointer 返 false
  2. 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();
    }
}

看删除就能看出来关联对象是如何管理的,这里复习一下:

  1. 有一个单例 AssociationsManager
  2. 通过 get() 获取了一个 AssociationsHashMap
  3. 这个 AssociationsHashMap 里面 K:V 是 disguise(obj)ObjectAssociation
  4. ObjectAssociation:结构体存储着:policyvalue

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 图,好久好久之前画的。

OC底层对象Dealloc流程图

你可能感兴趣的:(面试遇到的 OC 的 dealloc() 问题思考)