動(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í)主要有兩種思路:
借助hook,替換
dealloc方法。但是dealloc是NSObjec的方法,若要hook該方法,會(huì)對(duì)所有的cocoa實(shí)例產(chǎn)生影響,而我的實(shí)際目標(biāo)只有UITextField,顯然這種方式不太妙。而且事實(shí)上,ARC下是無法直接hookdealloc方法的(通過運(yùn)行時(shí)可以實(shí)現(xiàn)),會(huì)產(chǎn)生編譯報(bào)錯(cuò):ARC forbids use of 'dealloc' in a @selector。因此,這種方案Pass!-
借助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 的問題:
- 對(duì)象的實(shí)例變量如何釋放?
- 父類中的對(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)聽:

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

可見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)用棧如下:

可見子類的 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(); }可以看到做了兩件事:
- 將對(duì)象弱引用表清空,即將弱引用該對(duì)象的指針置為nil
- 清空引用計(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]

可以看到確實(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