面試遇到的 OC 的 dealloc() 問題思考

0. 前言

最近面了一些試,某位面試官問了我一個(gè)有意思的問題:

dealloc 的時(shí)候增加引用計(jì)數(shù),會(huì)防止對(duì)象銷毀嗎?

當(dāng)時(shí)猜測(cè)了一下,沒答很全面,今晚有空了,好好梳理一下 delloc 的流程。

1. 調(diào)用Dealloc之前的流程
2. Dealloc()
2.1 析構(gòu) CxxDestruct,以及到底是什么!
2.2 移除關(guān)聯(lián)對(duì)象,復(fù)習(xí)關(guān)聯(lián)對(duì)象實(shí)現(xiàn)
3. clearDeallocationg(),處理weak
4. free(obj)
5. 父類的 Dealloc 呢?

穿插研究復(fù)習(xí)幾個(gè)問題,提供幾個(gè)真正屬于 runtime 的面試題:

1. customRR是什么?
2. SideTable里操作Weak在什么環(huán)節(jié)?
3. CxxDestruct是什么?
4. 源碼里沒有,那父類Dealloc如何調(diào)用的?
5. 一個(gè)Strong一個(gè)weak在delloc里面指向?qū)ο笞约簳?huì)怎么樣?
6. 為什么自己寫 dealloc 里面不需要寫 [super dealloc]?

1. 調(diào)用Dealloc之前的流程

首先創(chuàng)造一個(gè)簡(jiǎn)單對(duì)象的 delloc 流程下符號(hào)斷點(diǎn) delloc :

{
      Fruit *banana = [[Fruit alloc] init];
}

可以捕獲斷點(diǎn) 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

可以看到進(jìn)入的流程:objc_storeStrong --> _objc_release --> release ......這就夠了,最新的代碼走起:

// PS: 如果不喜歡 bt 這個(gè)詞的話,可以直接看:
// [Always Show Disassembly]
// 總之找到這個(gè) 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

調(diào)用了 NSObject.mmobjc_storeStrong()方法,傳入的參數(shù)是 對(duì)象的地址以及 obj 為 nil,其實(shí)就是一個(gè)賦空值的過程。

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    // 判斷上一個(gè)值跟目前的值是否同一地址
    if (obj == prev) {
        return;
    }
    // 兩個(gè)值,retain新值,release舊的值
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

很明顯這個(gè) objc_retain(nil); 沒什么作用,重點(diǎn)就是 objc_release(banana);

objc_release() 方法的實(shí)現(xiàn)很簡(jiǎn)單,第一判空返回,第二判TaggedPointer返回,第三調(diào)用obj->release()。

__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

繼續(xù)調(diào)用 objc_object::release()

// 等同于調(diào)用 [this release], 如果沒有重寫的話,就走一個(gè)捷徑
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 實(shí)現(xiàn)了默認(rèn)的 retain/release/autorelease/retainCount/ _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法

//class 或 superclass 有默認(rèn)的 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 大多數(shù)情況會(huì)調(diào)用 rootRelease,如果你的類自己實(shí)現(xiàn)了RR方法,就給 msgSend release 方法。

繼續(xù)跟蹤 rootRelease()

ALWAYS_INLINE bool 
objc_object::rootRelease()
{
    return rootRelease(true, false);
}

rootRelease(true, false); 代碼太多,看圖里有詳情,這里解釋一下做的事情:

  1. 判 Tagged Pointer 返 false
  2. sideTable 和 extra_rc 的引用計(jì)數(shù)合并,如果都沒有了,就通過 msgSend dealloc

另外這里提到:sidetable 使用的鎖是自旋

問題1:Tagged Pointer 為啥這個(gè)時(shí)候返回False
TP不參與任何的引用計(jì)數(shù),引用本身即是值,因?yàn)樗軉渭?,指針混淆、身體里大部分都是值。講TP文章太多,在此不贅述了。

問題2:為什么會(huì)有 sideTable 和 extra_rc?
首先:nonpointerISA 對(duì)象的引用計(jì)數(shù)優(yōu)先放在isa里面的ExtraRc里面,直到突破了那個(gè)區(qū)域的限制,就轉(zhuǎn)到 SideTable 中的 兩個(gè)Map中存儲(chǔ)。
其次:而 pointerISA 直接在 SideTable 中的 兩個(gè)Map中存儲(chǔ)。

問題3:開篇的面試題
沒有用,會(huì)繼續(xù)釋放,因?yàn)檫M(jìn)入 dealloc 之前,問題2,已經(jīng)判斷了引用計(jì)數(shù)。

引發(fā)了另一個(gè)我想到的問題
一個(gè)Strong一個(gè)weak在delloc里面指向?qū)ο笞约?,?huì)怎么樣?稍后嘗試!?。?br> 我這里根據(jù)下面的推測(cè)猜測(cè)一下,strong 可能會(huì)有問題,甚至可能野指針,weak的話后面會(huì)釋放,所以沒關(guān)系。

2. Dealloc()

通過msgSend進(jìn)入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);
    }
}

好如果你這個(gè)對(duì)象:是nonpointter的話,請(qǐng)問你是否有弱引用或者你是否有關(guān)聯(lián)對(duì)象或者是否有析構(gòu)函數(shù)或者是否你的引用計(jì)數(shù)太打了,存到sidetable里面去了。

以上問題,如果你有一項(xiàng)的話,跟我們走一趟,調(diào)用下面的object_dispose((id)this)

如果以上你都沒有,那好你是一個(gè)很單純的對(duì)象,恭喜你,free() and go!

另外,去執(zhí)行 object_dispose((id)this) 的對(duì)象也不要擔(dān)心,看下面的實(shí)現(xiàn),只是在 free(obj)之前多調(diào)用了一個(gè) 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 這個(gè)是本篇的核心方法:

/************************************
* 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 析構(gòu) CxxDestruct,以及到底是什么!

你看上面的源碼說了,先執(zhí)行析構(gòu)函數(shù)還是先刪除關(guān)聯(lián)對(duì)象,先析構(gòu)再移除關(guān)聯(lián)。

object_cxxDestruct(obj) 往下走是 objc-class.mm 這個(gè)文件:兩個(gè)方法,代碼就不全粘了,核心就是一個(gè)for循環(huán):

    // 源:先調(diào)用 cls 的析構(gòu),再不斷調(diào)用父類的析構(gòu)函數(shù)
    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);
        }
    }

看看這個(gè)for循環(huán)有意思,Cook多騷氣,for ( ; cls; cls = cls->superclass),然后里面判斷了hasCxxDtor,沒有C++Dtor的話人家不寫break,直接return了。

如果這個(gè)for循環(huán)找到了C++析構(gòu),就調(diào)用lookupMethodInClassAndLoadCache,方法尋址,這個(gè)方法有注釋,僅在構(gòu)造和析構(gòu)的時(shí)候使用,也做了斷言,很安全。

內(nèi)部實(shí)現(xiàn)是一個(gè)方法尋址,找到的話填充緩存返回。

關(guān)于這個(gè)C++的析構(gòu)函數(shù),這里詳細(xì)說明下,這里詳細(xì)釋放了對(duì)象的每一個(gè)實(shí)例變量!所以這個(gè)方法一點(diǎn)是 LLVM 的 clang 中的 代碼生成模塊搞出來的。

http://clang.llvm.org/doxygen/CodeGenModule_8cpp-source.html

它的方法實(shí)現(xiàn)核心就是對(duì)于這個(gè)對(duì)象所有實(shí)例變量,遍歷調(diào)用objc_storeStrong(),然后這個(gè)實(shí)例變量就解除retain 了。

id objc_storeStrong(id *object, id value) {
  value = [value retain];
  id oldValue = *object;
  *object = value;
  [oldValue release];
  return value;
}

2.2 移除關(guān)聯(lián)對(duì)象,復(fù)習(xí)關(guān)聯(lián)對(duì)象實(shí)現(xiàn)

看到這里你要知道,我們通過API添加的關(guān)聯(lián)對(duì)象,不管你ARC還是MRC,都沒必要手動(dòng)Remove。

objc-references.mm 里面

// 與設(shè)置/獲取關(guān)聯(lián)的引用不同,
// 此函數(shù)對(duì)性能敏感,
// 因?yàn)樵嫉膇sa對(duì)象(例如OS對(duì)象)
// 無法跟蹤它們是否具有關(guān)聯(lián)的對(duì)象。
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();
    }
}

看刪除就能看出來關(guān)聯(lián)對(duì)象是如何管理的,這里復(fù)習(xí)一下:

  1. 有一個(gè)單例 AssociationsManager
  2. 通過 get() 獲取了一個(gè) AssociationsHashMap
  3. 這個(gè) AssociationsHashMap 里面 K:V 是 disguise(obj)ObjectAssociation
  4. ObjectAssociation:結(jié)構(gòu)體存儲(chǔ)著:policy,value

3. clearDeallocationg(),處理weak

繼續(xù)前面的流程,完成2.1 2.2 之后 調(diào)用了 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 分開兩個(gè)邏輯。

通過這個(gè)對(duì)象取出 Side Table,處理里面的 weakTable,調(diào)用 weak_clear_no_lock(&table.weak_table, (id)this)table.refcnts.erase(it)完成清除。

將所有weak引用指nil,就是在這里實(shí)現(xiàn)的。

最后有一個(gè)debug的斷言,專門寫一個(gè)函數(shù)來做debug的斷言,這個(gè)也是非常謹(jǐn)慎的體現(xiàn),粘出來紀(jì)念一下Cook做飯的謹(jǐn)慎:

// NSObject.mm
#if DEBUG
//DEBUG 模式下才啟用 :用于  斷言 side table 中不存在對(duì)象。
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)

最后不管對(duì)象付不復(fù)雜,都會(huì)調(diào)用 free(obj)

Free 的實(shí)現(xiàn)在malloc的源碼里面:

5. 父類的 Dealloc 呢?

至今看源碼看不到任何調(diào)用父類dealloc 的地方,這個(gè)網(wǎng)上搜了一下,發(fā)現(xiàn)結(jié)果在這里!

http://clang.llvm.org/doxygen/CGObjC_8cpp_source.html

clang 在 ARC 的 dealloc 結(jié)束的時(shí)候插入了如雷的dealloc 的調(diào)用。

通過 forward 給 superclass 調(diào)用 dealloc,才實(shí)現(xiàn)的[super dealloc]操作。

這個(gè)時(shí)候上面有一個(gè)隱藏的問題也就解釋了,其實(shí) msgSend 來調(diào)用 dealloc 方法,會(huì)先調(diào)用 Fruit 的 dealloc (如果有的話),然后因?yàn)镃langCodeGen的插手,才有的調(diào)用父類一直到根類的 dealloc。

所有dealloc不需要我們手動(dòng)寫 super dealloc。

6 總結(jié)圖

最后放上來一張 dealloc 圖,好久好久之前畫的。

OC底層對(duì)象Dealloc流程圖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容