探索dealloc真諦
動機由來
最近在封裝一個 UITextField
分類的時候遇到了一個問題,大致需求是封裝 UITextField
的若干功能,方便業務方這樣使用:
// 限制輸入長度 [_tf ltv_limitLength:5]; // 限制輸入字元 [_tf ltv_limitContent:[NSCharacterSet characterSetWithCharactersInString:@"-+*"]]; // 匹配輸入條件觸發action [_tf ltv_matchCondition:^BOOL(NSString *text) { return [text isEqualToString:@"asd"]; } action:^(NSString *text) { NSLog(@"matched asd"); }];
基本實現思路是藉助一個全域性單例,作為UITextField內容變化時通知的觀察者,其中object引數指定了需要監聽的 UITextField
例項,這樣一來,當輸入內容發生變化,就能觸發對應 UITextField
例項相關的邏輯處理:
[[NSNotificationCenter defaultCenter] addObserver:[self manager] selector:@selector(textfieldDidChangedTextNotification:) name:UITextFieldTextDidChangeNotification object:target];
這種思路有一個問題需要處理,就是當 UITextField
例項釋放的時候,需要移除對應的通知。也就是說,我需要監聽 UITextField
例項的釋放。由於是系統控制元件,沒法直接複寫 dealloc
方法,因此需要藉助一些執行時魔法。當時主要有兩種思路:
-
藉助hook,替換
dealloc
方法。但是dealloc
是NSObjec的方法,若要hook該方法,會對所有的cocoa例項產生影響,而我的實際目標只有UITextField,顯然這種方式不太妙。而且事實上,ARC下是無法直接hookdealloc
方法的(通過執行時可以實現),會產生編譯報錯:ARC forbids use of 'dealloc' in a @selector
。因此,這種方案Pass! -
藉助AssociatedObject。我們知道,ARC下,一個例項釋放後,同時會解除對其例項變數的強引用。這樣一來,我就可以通過AssociatedObject動態給UITextField例項繫結一個自定義的輔助物件,並且監聽該輔助物件的
dealloc
方法呼叫。因為按照我的理論,當UITextField例項被釋放後,輔助物件唯一的強引用被解除,必然將觸發dealloc
的呼叫。這樣一來,我就能夠間接監聽宿主UITextField例項的釋放了。然而,想法很美好,現實略骨感。我確實能夠監聽UITextField例項的釋放了,然而似乎忘記了我真正的意圖——真正要做的是在UITextField例項被釋放之前拿到例項本身,呼叫方法移除對應的通知:
[[NSNotificationCenter defaultCenter] removeObserver:[self manager] name:UITextFieldTextDidChangeNotification object:target]
我忽略了一個很重要的問題:當例項變數的
dealloc
方法呼叫的時候,其宿主物件已經被釋放了,也就是說在例項變數的dealloc
方法中已經拿不到宿主物件了。因此我還是拿不到UITextField例項!!Pass!!
這個問題似乎沒有很好的解決方案,最終換了一種思路:不再為每個UITextField例項繫結觀察者監聽通知,而是註冊一個全域性的通知:
[[NSNotificationCenter defaultCenter] addObserver:[self manager] selector:@selector(textfieldDidChangedTextNotification:) name:UITextFieldTextDidChangeNotification object:nil];
在監聽通知的回撥方法中判斷觸發通知的UITextField例項是否是需要處理的例項,僅在命中的時候進行邏輯處理。
- (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(); }]; } }
這種方案雖然有個顯而易見的缺陷(會監聽所有的UITextField例項),但是個人認為比hook dealloc方法要好,首先受眾物件只限定在UITextField,其次多餘的邏輯處理較為簡單,不會產生較大的效能影響。另外,想了想IQKeyBoard也是全域性監聽UITextField,問題應該不大吧~ 如果你有更好的方案,歡迎來撩~
雖然眼前問題是解決了,但是此時內心已經暗戳戳萌芽了一個更大的困惑: dealloc方法到底幹了啥?
進入正題
首先,我們都知道當一個物件的引用計數為0的時候,就會呼叫 dealloc
方法進行析構。在MRC時代,記憶體需要手動管理,解除物件引用需要手動調 release
,通常也會這樣寫 dealloc
:
- (void)dealloc { self.instance1 = nil; self.instance2 = nil; // ... // 非cocoa物件記憶體的釋放,如CF物件 // ... [super dealloc]; }
[super dealloc]
而到了ARC時代, dealloc
基本變成了這樣:
- (void)dealloc { // ... // 非cocoa物件記憶體的釋放,如CF物件 // ... }
除了非cocoa物件還需要手動釋放,例項變數釋放和 [super dealloc]
都不見了身影。這也就是我們要探索的兩個ARC下 dealloc
的問題:
- 物件的例項變數如何釋放?
- 父類中的物件析構如何實現?
初探dealloc的呼叫
當探索一個方法無從下手時,最好的方法就是檢視呼叫棧,說不定就能從中窺見一二。測試程式碼如下:
// 父類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 // 例項變數型別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; }
執行工程,由於dog例項很快過了作用域,因此會觸發例項的釋放。列印的日誌如下:
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方法中儘管沒呼叫 [super dealloc]
,也沒有手動釋放對例項變數skill的引用,父類Animal的 dealloc
和例項變數skill的 dealloc
方法最終都呼叫了。
由於觸發物件呼叫dealloc的直接原因是物件引用計數為0,而例項變數實際上是被 dog.skill
這個變數所持有,因此可以通過 Watchpoint 來監聽skill變數的記憶體變化。在main函式的 return 0;
語句上打個斷點,然後通過 watchpoint set variable dog->_skill
設定監聽:

image
繼續執行,隨後就能監聽到skill記憶體的變化:

image
可見dog的skill例項變數的記憶體地址從 0x00000001007661b0 變成了 0x0000000000000000,也就是說這個時間節點skill物件被釋放了(其實嚴格來說這麼說是不正確的,此時堆上的skill物件並沒有被釋放,我們監聽到的只是棧上的skill變數值被清掉了,因此也就無法再通過變數訪問該物件了)。
此時呼叫棧如下:

image
可見子類的 dealloc
呼叫之後,父類的跟著呼叫。隨後通過一系列執行時方法,最終在一個名為 .cxx_destruct
的方法中呼叫了 objc_storeStrong
來完成釋放工作。另外可以看到這個 .cxx_destruct
是Animal的方法,怎麼來的呢?執行時都做了些什麼事?帶著這些疑問繼續往下看。
NSObject的dealloc實現
是時候來看一下runtime中相關的實現了,runtime原始碼可以在 ofollow,noindex">Source Browser 下載。
經過定位和呼叫追蹤,發現經過了如下函式:
dealloc -> _objc_rootDealloc -> object_dispose -> objc_destructInstance
前面都是些簡單的判斷和跳轉,重要的是 objc_destructInstance
函式:
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; }
可以看到這個函式主要做了3件事:
-
object_cxxDestruct
這個函式有點眼熟,跟剛才呼叫棧中看到的
.cxx_destruct
長得很像,猜測例項變數釋放以及呼叫父類的dealloc
都是在這裡面進行的。 -
_object_remove_assocations
顧名思義,用來釋放動態繫結的物件。
-
clearDeallocating
該函式實現如下:
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(); }
可以看到做了兩件事:
- 將物件弱引用表清空,即將弱引用該物件的指標置為nil
- 清空引用計數表(當一個物件的引用計數值過大(超過255)時,引用計數會儲存在一個叫
SideTable
的屬性中,此時isa的has_sidetable_rc
值為1)
接下來,要探索的就是 object_cxxDestruct
函數了,實現如下:
void object_cxxDestruct(id obj) { if (!obj) return; if (obj->isTaggedPointer()) return; object_cxxDestructFromClass(obj, obj->ISA()); }
object_cxxDestructFromClass
這個函式之前在呼叫棧裡看到過,再往裡看:
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);
執行的其實是 SEL_cxx_destruct
這個SEL標記的函式,通過全域性搜尋 SEL_cxx_destruct
,不難發現該SEL對應的正是之前看到的 .cxx_destruct 方法,也就是說,最終是 .cxx_destruct
方法被呼叫了。
探索.cxx_destruct方法
之前在呼叫棧中看到該方法是Animal類中的方法,而我們並沒有申明該方法,也沒有動態插入該方法的相關程式碼。並且這個方法是析構物件相關的,具有很強的通用性,那麼猜測是在編譯的時候由前端編譯器(clang)自動插入的。
我們可以通過 DLIntrospection 來檢視Animal類中是否真的存在這個方法,該工具可以方便在lldb中列印類中所有的例項變數、方法、物件遵守的協議等資訊,是一個NSObject的分類檔案,直接拉到工程中即可使用。
在main函式中打個斷點,然後在lldb中列印Animal類的例項方法:
po [[Animal class] instanceMethods]

image
可以看到確實是有 .css_destruct
這個方法。隨後,通過查閱相關資料,驗證了我之前的猜測。在clang原始碼裡,找到了相關的程式碼:
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模組 中看到了上面程式碼(只摘錄了相關程式碼),經過分析大概是clang通過CodeGen為具體類插入了 .cxx_destruct
方法。 GenerateObjCCtorDtorMethod
函式實現在 CGObjC.cpp 檔案中,其中聲明瞭 .cxx_destruct
的具體實現。最終物件釋放時,會呼叫到 emitCXXDestructMethod
函式:
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?"); }
經過分析,該函式做的事情是:遍歷所有例項變數,呼叫 destroyARCStrongWithStore
。而 destroyARCStrongWithStore
最終呼叫的就是之前呼叫棧中看到的 objc_storeStrong
函式,可以在runtime原始碼中看到其實現:
void objc_storeStrong(id *location, id obj) { id prev = *location; if (obj == prev) { return; } objc_retain(obj); *location = obj; objc_release(prev); }
該函式作用是將obj物件賦值給location變數,因此只要執行 objc_storeStrong(&ivar, null)
就能釋放ivar例項變數。至此, dealloc
方法如何釋放例項變數這個問題就探索完畢了。
至於如何呼叫 [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
方法中插入了程式碼,相關程式碼在 FinishARCDealloc
結構中定義:
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); } }; }
大致意思就是呼叫父類的 dealloc
方法。
撥雲見日
通過上面的探索分析,基本搞清楚了ARC下 dealloc
是怎麼實現自動釋放例項變數以及呼叫父類 dealloc
方法的。這一切要歸功於clang以及執行時庫,在前端編譯過程中CodeGen插入了相關程式碼,結合執行時完成釋放動作。對於ARC下 dealloc
實現原理的摸索就此告終。
測試demo地址: https://github.com/Lotheve/blogdemo/tree/master/DeallocExporeDemo