探索dealloc真諦

動(dòng)機(jī)由來

最近在封裝一個(gè) UITextField 分類的時(shí)候遇到了一個(gè)問題,大致需求是封裝 UITextField 的若干功能,方便業(yè)務(wù)方這樣使用:

// 限制輸入長(zhǎng)度
[_tf ltv_limitLength:5];
// 限制輸入字符
[_tf ltv_limitContent:[NSCharacterSet characterSetWithCharactersInString:@"-+*"]];
// 匹配輸入條件觸發(fā)action
[_tf ltv_matchCondition:^BOOL(NSString *text) {
    return [text isEqualToString:@"asd"];
} action:^(NSString *text) {
    NSLog(@"matched asd");
}];

基本實(shí)現(xiàn)思路是借助一個(gè)全局單例,作為UITextField內(nèi)容變化時(shí)通知的觀察者,其中object參數(shù)指定了需要監(jiān)聽的 UITextField 實(shí)例,這樣一來,當(dāng)輸入內(nèi)容發(fā)生變化,就能觸發(fā)對(duì)應(yīng) UITextField 實(shí)例相關(guān)的邏輯處理:

[[NSNotificationCenter defaultCenter] addObserver:[self manager] selector:@selector(textfieldDidChangedTextNotification:) name:UITextFieldTextDidChangeNotification object:target];

這種思路有一個(gè)問題需要處理,就是當(dāng) UITextField 實(shí)例釋放的時(shí)候,需要移除對(duì)應(yīng)的通知。也就是說,我需要監(jiān)聽 UITextField 實(shí)例的釋放。由于是系統(tǒng)控件,沒法直接復(fù)寫 dealloc 方法,因此需要借助一些運(yùn)行時(shí)魔法。當(dāng)時(shí)主要有兩種思路:

  1. 借助hook,替換 dealloc 方法。但是 dealloc 是NSObjec的方法,若要hook該方法,會(huì)對(duì)所有的cocoa實(shí)例產(chǎn)生影響,而我的實(shí)際目標(biāo)只有UITextField,顯然這種方式不太妙。而且事實(shí)上,ARC下是無法直接hook dealloc 方法的(通過運(yùn)行時(shí)可以實(shí)現(xiàn)),會(huì)產(chǎn)生編譯報(bào)錯(cuò):ARC forbids use of 'dealloc' in a @selector。因此,這種方案Pass!

  2. 借助AssociatedObject。我們知道,ARC下,一個(gè)實(shí)例釋放后,同時(shí)會(huì)解除對(duì)其實(shí)例變量的強(qiáng)引用。這樣一來,我就可以通過AssociatedObject動(dòng)態(tài)給UITextField實(shí)例綁定一個(gè)自定義的輔助對(duì)象,并且監(jiān)聽該輔助對(duì)象的 dealloc 方法調(diào)用。因?yàn)榘凑瘴业睦碚摚?dāng)UITextField實(shí)例被釋放后,輔助對(duì)象唯一的強(qiáng)引用被解除,必然將觸發(fā) dealloc 的調(diào)用。這樣一來,我就能夠間接監(jiān)聽宿主UITextField實(shí)例的釋放了。

    然而,想法很美好,現(xiàn)實(shí)略骨感。我確實(shí)能夠監(jiān)聽UITextField實(shí)例的釋放了,然而似乎忘記了我真正的意圖——真正要做的是在UITextField實(shí)例被釋放之前拿到實(shí)例本身,調(diào)用方法移除對(duì)應(yīng)的通知:

    [[NSNotificationCenter defaultCenter] removeObserver:[self manager] name:UITextFieldTextDidChangeNotification object:target]
    

    我忽略了一個(gè)很重要的問題:當(dāng)實(shí)例變量的 dealloc 方法調(diào)用的時(shí)候,其宿主對(duì)象已經(jīng)被釋放了,也就是說在實(shí)例變量的 dealloc 方法中已經(jīng)拿不到宿主對(duì)象了。因此我還是拿不到UITextField實(shí)例??!Pass??!

這個(gè)問題似乎沒有很好的解決方案,最終換了一種思路:不再為每個(gè)UITextField實(shí)例綁定觀察者監(jiān)聽通知,而是注冊(cè)一個(gè)全局的通知:

[[NSNotificationCenter defaultCenter] addObserver:[self manager] selector:@selector(textfieldDidChangedTextNotification:) name:UITextFieldTextDidChangeNotification object:nil];

在監(jiān)聽通知的回調(diào)方法中判斷觸發(fā)通知的UITextField實(shí)例是否是需要處理的實(shí)例,僅在命中的時(shí)候進(jìn)行邏輯處理。

- (void)textfieldDidChangedTextNotification:(NSNotification *)notification
{
    UITextField *textField = (UITextField *)notification.object;
    if ([_targetTable containsObject:textField]) {
        [textField.operations enumerateObjectsUsingBlock:^(LTVTFOperation * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            obj.action();
        }];
    }
}

這種方案雖然有個(gè)顯而易見的缺陷(會(huì)監(jiān)聽所有的UITextField實(shí)例),但是個(gè)人認(rèn)為比hook dealloc方法要好,首先受眾對(duì)象只限定在UITextField,其次多余的邏輯處理較為簡(jiǎn)單,不會(huì)產(chǎn)生較大的性能影響。另外,想了想IQKeyBoard也是全局監(jiān)聽UITextField,問題應(yīng)該不大吧~ 如果你有更好的方案,歡迎來撩~

雖然眼前問題是解決了,但是此時(shí)內(nèi)心已經(jīng)暗戳戳萌芽了一個(gè)更大的困惑:dealloc方法到底干了啥?

進(jìn)入正題

首先,我們都知道當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)為0的時(shí)候,就會(huì)調(diào)用 dealloc 方法進(jìn)行析構(gòu)。在MRC時(shí)代,內(nèi)存需要手動(dòng)管理,解除對(duì)象引用需要手動(dòng)調(diào) release ,通常也會(huì)這樣寫 dealloc

- (void)dealloc {
    self.instance1 = nil;
    self.instance2 = nil;
    // ...
    // 非cocoa對(duì)象內(nèi)存的釋放,如CF對(duì)象
    // ...
    [super dealloc];
}
  • 移除對(duì)相關(guān)實(shí)例的引用
  • 非cocoa對(duì)象的釋放
  • 調(diào)用 [super dealloc] 來釋放父類中的對(duì)象

而到了ARC時(shí)代,dealloc 基本變成了這樣:

- (void)dealloc {
    // ...
    // 非cocoa對(duì)象內(nèi)存的釋放,如CF對(duì)象
    // ...
}

除了非cocoa對(duì)象還需要手動(dòng)釋放,實(shí)例變量釋放和 [super dealloc] 都不見了身影。這也就是我們要探索的兩個(gè)ARC下 dealloc 的問題:

  1. 對(duì)象的實(shí)例變量如何釋放?
  2. 父類中的對(duì)象析構(gòu)如何實(shí)現(xiàn)?

初探dealloc的調(diào)用

當(dāng)探索一個(gè)方法無從下手時(shí),最好的方法就是查看調(diào)用棧,說不定就能從中窺見一二。測(cè)試代碼如下:

// 父類Animal
@interface Animal : NSObject
@property (nonatomic, strong) Skill *skill;
@end
@implementation Animal
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end

// 子類Dog
@interface Dog : Animal
@end
@implementation Dog
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end

// 實(shí)例變量類型Skill
@interface Skill : NSObject
@end
@implementation Skill
- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end
    
int main(int argc, const char * argv[]) {
    Dog *dog = [Dog new];
    dog.skill = [Skill new];
    return 0;
}

運(yùn)行工程,由于dog實(shí)例很快過了作用域,因此會(huì)觸發(fā)實(shí)例的釋放。打印的日志如下:

2018-11-01 17:09:43.986073+0800 DeallocExporeDemo[5674:1072191] -[Dog dealloc]
2018-11-01 17:09:43.986302+0800 DeallocExporeDemo[5674:1072191] -[Animal dealloc]
2018-11-01 17:09:45.751398+0800 DeallocExporeDemo[5674:1072191] -[Skill dealloc]

可見雖然dealloc方法中盡管沒調(diào)用 [super dealloc] ,也沒有手動(dòng)釋放對(duì)實(shí)例變量skill的引用,父類Animal的 dealloc 和實(shí)例變量skill的 dealloc 方法最終都調(diào)用了。

由于觸發(fā)對(duì)象調(diào)用dealloc的直接原因是對(duì)象引用計(jì)數(shù)為0,而實(shí)例變量實(shí)際上是被 dog.skill 這個(gè)變量所持有,因此可以通過 Watchpoint 來監(jiān)聽skill變量的內(nèi)存變化。在main函數(shù)的 return 0; 語句上打個(gè)斷點(diǎn),然后通過 watchpoint set variable dog->_skill 設(shè)置監(jiān)聽:

image

繼續(xù)執(zhí)行,隨后就能監(jiān)聽到skill內(nèi)存的變化:

image

可見dog的skill實(shí)例變量的內(nèi)存地址從 0x00000001007661b0 變成了 0x0000000000000000,也就是說這個(gè)時(shí)間節(jié)點(diǎn)skill對(duì)象被釋放了(其實(shí)嚴(yán)格來說這么說是不正確的,此時(shí)堆上的skill對(duì)象并沒有被釋放,我們監(jiān)聽到的只是棧上的skill變量值被清掉了,因此也就無法再通過變量訪問該對(duì)象了)。

此時(shí)調(diào)用棧如下:

image

可見子類的 dealloc 調(diào)用之后,父類的跟著調(diào)用。隨后通過一系列運(yùn)行時(shí)方法,最終在一個(gè)名為 .cxx_destruct 的方法中調(diào)用了 objc_storeStrong 來完成釋放工作。另外可以看到這個(gè) .cxx_destruct 是Animal的方法,怎么來的呢?運(yùn)行時(shí)都做了些什么事?帶著這些疑問繼續(xù)往下看。

NSObject的dealloc實(shí)現(xiàn)

是時(shí)候來看一下runtime中相關(guān)的實(shí)現(xiàn)了,runtime源碼可以在 Source Browser 下載。

經(jīng)過定位和調(diào)用追蹤,發(fā)現(xiàn)經(jīng)過了如下函數(shù):

dealloc -> _objc_rootDealloc -> object_dispose -> objc_destructInstance

前面都是些簡(jiǎn)單的判斷和跳轉(zhuǎn),重要的是 objc_destructInstance 函數(shù):

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;
}

可以看到這個(gè)函數(shù)主要做了3件事:

  • object_cxxDestruct

    這個(gè)函數(shù)有點(diǎn)眼熟,跟剛才調(diào)用棧中看到的 .cxx_destruct 長(zhǎng)得很像,猜測(cè)實(shí)例變量釋放以及調(diào)用父類的dealloc都是在這里面進(jìn)行的。

  • _object_remove_assocations

    顧名思義,用來釋放動(dòng)態(tài)綁定的對(duì)象。

  • clearDeallocating

    該函數(shù)實(shí)現(xiàn)如下:

    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());
    }
    
    NEVER_INLINE void objc_object::clearDeallocating_slow()
    {
        assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
        SideTable& table = SideTables()[this];
        table.lock();
        if (isa.weakly_referenced) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        if (isa.has_sidetable_rc) {
            table.refcnts.erase(this);
        }
        table.unlock();
    }
    

    可以看到做了兩件事:

    1. 將對(duì)象弱引用表清空,即將弱引用該對(duì)象的指針置為nil
    2. 清空引用計(jì)數(shù)表(當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)值過大(超過255)時(shí),引用計(jì)數(shù)會(huì)存儲(chǔ)在一個(gè)叫 SideTable 的屬性中,此時(shí)isa的 has_sidetable_rc 值為1)

接下來,要探索的就是 object_cxxDestruct 函數(shù)了,實(shí)現(xiàn)如下:

void object_cxxDestruct(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    object_cxxDestructFromClass(obj, obj->ISA());
}

object_cxxDestructFromClass 這個(gè)函數(shù)之前在調(diào)用棧里看到過,再往里看:

static void object_cxxDestructFromClass(id obj, Class cls)
{
    void (*dtor)(id);

    // Call cls's dtor first, then superclasses's dtors.

    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);
        }
    }
}

通過分析,最終 (*dtor)(obj); 執(zhí)行的其實(shí)是 SEL_cxx_destruct 這個(gè)SEL標(biāo)記的函數(shù),通過全局搜索 SEL_cxx_destruct ,不難發(fā)現(xiàn)該SEL對(duì)應(yīng)的正是之前看到的 .cxx_destruct 方法,也就是說,最終是 .cxx_destruct 方法被調(diào)用了。

探索.cxx_destruct方法

之前在調(diào)用棧中看到該方法是Animal類中的方法,而我們并沒有申明該方法,也沒有動(dòng)態(tài)插入該方法的相關(guān)代碼。并且這個(gè)方法是析構(gòu)對(duì)象相關(guān)的,具有很強(qiáng)的通用性,那么猜測(cè)是在編譯的時(shí)候由前端編譯器(clang)自動(dòng)插入的。

我們可以通過 DLIntrospection 來查看Animal類中是否真的存在這個(gè)方法,該工具可以方便在lldb中打印類中所有的實(shí)例變量、方法、對(duì)象遵守的協(xié)議等信息,是一個(gè)NSObject的分類文件,直接拉到工程中即可使用。

在main函數(shù)中打個(gè)斷點(diǎn),然后在lldb中打印Animal類的實(shí)例方法:

po [[Animal class] instanceMethods]
image

可以看到確實(shí)是有 .css_destruct 這個(gè)方法。隨后,通過查閱相關(guān)資料,驗(yàn)證了我之前的猜測(cè)。在clang源碼里,找到了相關(guān)的代碼:

void CodeGenModule::EmitObjCIvarInitializations(ObjCImplementationDecl *D) {
    IdentifierInfo *II = &getContext().Idents.get(".cxx_destruct");
    Selector cxxSelector = getContext().Selectors.getSelector(0, &II);
    ObjCMethodDecl *DTORMethod =
    ObjCMethodDecl::Create(getContext(), D->getLocation(), D->getLocation(),
                          cxxSelector, getContext().VoidTy, nullptr, D,
                          /isInstance=/true, /isVariadic=/false,
                       /isPropertyAccessor=/true, /isImplicitlyDeclared=/true,
                          /isDefined=/false, ObjCMethodDecl::Required);
    D->addInstanceMethod(DTORMethod);
    CodeGenFunction(*this).GenerateObjCCtorDtorMethod(D, DTORMethod, false);
}

在clang的CodeGenModule模塊中看到了上面代碼(只摘錄了相關(guān)代碼),經(jīng)過分析大概是clang通過CodeGen為具體類插入了 .cxx_destruct 方法。 GenerateObjCCtorDtorMethod 函數(shù)實(shí)現(xiàn)在 CGObjC.cpp 文件中,其中聲明了 .cxx_destruct 的具體實(shí)現(xiàn)。最終對(duì)象釋放時(shí),會(huì)調(diào)用到 emitCXXDestructMethod 函數(shù):

 static void emitCXXDestructMethod(CodeGenFunction &CGF,
                                   ObjCImplementationDecl *impl) {
   CodeGenFunction::RunCleanupsScope scope(CGF);
   llvm::Value *self = CGF.LoadObjCSelf();
   const ObjCInterfaceDecl *iface = impl->getClassInterface();
   for (const ObjCIvarDecl *ivar = iface->all_declared_ivar_begin();
        ivar; ivar = ivar->getNextIvar()) {
     QualType type = ivar->getType();
     // Check whether the ivar is a destructible type.
     QualType::DestructionKind dtorKind = type.isDestructedType();
     if (!dtorKind) continue;
     CodeGenFunction::Destroyer *destroyer = nullptr;
     // Use a call to objc_storeStrong to destroy strong ivars, for the
     // general benefit of the tools.
     if (dtorKind == QualType::DK_objc_strong_lifetime) {
       destroyer = destroyARCStrongWithStore;
     // Otherwise use the default for the destruction kind.
     } else {
       destroyer = CGF.getDestroyer(dtorKind);
     }
     CleanupKind cleanupKind = CGF.getCleanupKind(dtorKind);
     CGF.EHStack.pushCleanup<DestroyIvar>(cleanupKind, self, ivar, destroyer,
                                          cleanupKind & EHCleanup);
   }
   assert(scope.requiresCleanups() && "nothing to do in .cxx_destruct?");
 }

經(jīng)過分析,該函數(shù)做的事情是:遍歷所有實(shí)例變量,調(diào)用 destroyARCStrongWithStore 。而 destroyARCStrongWithStore 最終調(diào)用的就是之前調(diào)用棧中看到的 objc_storeStrong 函數(shù),可以在runtime源碼中看到其實(shí)現(xiàn):

void objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

該函數(shù)作用是將obj對(duì)象賦值給location變量,因此只要執(zhí)行 objc_storeStrong(&ivar, null) 就能釋放ivar實(shí)例變量。至此,dealloc 方法如何釋放實(shí)例變量這個(gè)問題就探索完畢了。

至于如何調(diào)用 [super dealloc] ,在clang源碼中同樣能找到貓膩。同樣在 CGObjC.cpp 文件中,存在如下代碼:

 void CodeGenFunction::StartObjCMethod(const ObjCMethodDecl *OMD,
                                       const ObjCContainerDecl *CD) {
   // In ARC, certain methods get an extra cleanup.
   if (CGM.getLangOpts().ObjCAutoRefCount &&
       OMD->isInstanceMethod() &&
       OMD->getSelector().isUnarySelector()) {
     const IdentifierInfo *ident =
       OMD->getSelector().getIdentifierInfoForSlot(0);
     if (ident->isStr("dealloc"))
       EHStack.pushCleanup<FinishARCDealloc>(getARCCleanupKind());
   }
 }

分析可知在 dealloc 方法中插入了代碼,相關(guān)代碼在 FinishARCDealloc 結(jié)構(gòu)中定義:

 namespace {
 struct FinishARCDealloc final : EHScopeStack::Cleanup {
   void Emit(CodeGenFunction &CGF, Flags flags) override {
     const ObjCMethodDecl *method = cast<ObjCMethodDecl>(CGF.CurCodeDecl);
 
     const ObjCImplDecl *impl = cast<ObjCImplDecl>(method->getDeclContext());
     const ObjCInterfaceDecl *iface = impl->getClassInterface();
     if (!iface->getSuperClass()) return;
 
     bool isCategory = isa<ObjCCategoryImplDecl>(impl);
 
     // Call [super dealloc] if we have a superclass.
     llvm::Value *self = CGF.LoadObjCSelf();
 
     CallArgList args;
     CGF.CGM.getObjCRuntime().GenerateMessageSendSuper(CGF, ReturnValueSlot(),
                                                       CGF.getContext().VoidTy,
                                                       method->getSelector(),
                                                       iface,
                                                       isCategory,
                                                       self,
                                                       /*is class msg*/ false,
                                                       args,
                                                       method);
   }
 };
 }

大致意思就是調(diào)用父類的 dealloc 方法。

撥云見日

通過上面的探索分析,基本搞清楚了ARC下 dealloc 是怎么實(shí)現(xiàn)自動(dòng)釋放實(shí)例變量以及調(diào)用父類 dealloc 方法的。這一切要?dú)w功于clang以及運(yùn)行時(shí)庫,在前端編譯過程中CodeGen插入了相關(guān)代碼,結(jié)合運(yùn)行時(shí)完成釋放動(dòng)作。對(duì)于ARC下 dealloc 實(shí)現(xiàn)原理的摸索就此告終。

測(cè)試demo地址:https://github.com/Lotheve/blogdemo/tree/master/DeallocExporeDemo

最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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