動機由來
最近在封裝一個 UITextField
分類的時候遇到了一個問題胰蝠,大致需求是封裝 UITextField
的若干功能塞茅,方便業(yè)務(wù)方這樣使用:
// 限制輸入長度
[_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");
}];
基本實現(xiàn)思路是借助一個全局單例亩码,作為UITextField內(nèi)容變化時通知的觀察者,其中object參數(shù)指定了需要監(jiān)聽的 UITextField
實例野瘦,這樣一來描沟,當(dāng)輸入內(nèi)容發(fā)生變化,就能觸發(fā)對應(yīng) UITextField
實例相關(guān)的邏輯處理:
[[NSNotificationCenter defaultCenter] addObserver:[self manager] selector:@selector(textfieldDidChangedTextNotification:) name:UITextFieldTextDidChangeNotification object:target];
這種思路有一個問題需要處理鞭光,就是當(dāng) UITextField
實例釋放的時候吏廉,需要移除對應(yīng)的通知。也就是說衰猛,我需要監(jiān)聽 UITextField
實例的釋放迟蜜。由于是系統(tǒng)控件刹孔,沒法直接復(fù)寫 dealloc
方法啡省,因此需要借助一些運行時魔法。當(dāng)時主要有兩種思路:
借助hook髓霞,替換
dealloc
方法卦睹。但是dealloc
是NSObjec的方法,若要hook該方法方库,會對所有的cocoa實例產(chǎn)生影響结序,而我的實際目標(biāo)只有UITextField,顯然這種方式不太妙纵潦。而且事實上徐鹤,ARC下是無法直接hookdealloc
方法的(通過運行時可以實現(xiàn)),會產(chǎn)生編譯報錯:ARC forbids use of 'dealloc' in a @selector
邀层。因此返敬,這種方案Pass!-
借助AssociatedObject寥院。我們知道劲赠,ARC下,一個實例釋放后,同時會解除對其實例變量的強引用凛澎。這樣一來霹肝,我就可以通過AssociatedObject動態(tài)給UITextField實例綁定一個自定義的輔助對象,并且監(jiān)聽該輔助對象的
dealloc
方法調(diào)用塑煎。因為按照我的理論沫换,當(dāng)UITextField實例被釋放后,輔助對象唯一的強引用被解除最铁,必然將觸發(fā)dealloc
的調(diào)用苗沧。這樣一來,我就能夠間接監(jiān)聽宿主UITextField實例的釋放了炭晒。然而待逞,想法很美好,現(xiàn)實略骨感网严。我確實能夠監(jiān)聽UITextField實例的釋放了识樱,然而似乎忘記了我真正的意圖——真正要做的是在UITextField實例被釋放之前拿到實例本身,調(diào)用方法移除對應(yīng)的通知:
[[NSNotificationCenter defaultCenter] removeObserver:[self manager] name:UITextFieldTextDidChangeNotification object:target]
我忽略了一個很重要的問題:當(dāng)實例變量的
dealloc
方法調(diào)用的時候震束,其宿主對象已經(jīng)被釋放了怜庸,也就是說在實例變量的dealloc
方法中已經(jīng)拿不到宿主對象了。因此我還是拿不到UITextField實例9复濉墓怀!PassT绿恕!
這個問題似乎沒有很好的解決方案,最終換了一種思路:不再為每個UITextField實例綁定觀察者監(jiān)聽通知服协,而是注冊一個全局的通知:
[[NSNotificationCenter defaultCenter] addObserver:[self manager] selector:@selector(textfieldDidChangedTextNotification:) name:UITextFieldTextDidChangeNotification object:nil];
在監(jiān)聽通知的回調(diào)方法中判斷觸發(fā)通知的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();
}];
}
}
這種方案雖然有個顯而易見的缺陷(會監(jiān)聽所有的UITextField實例)踱蛀,但是個人認為比hook dealloc方法要好,首先受眾對象只限定在UITextField馋辈,其次多余的邏輯處理較為簡單抚芦,不會產(chǎn)生較大的性能影響。另外迈螟,想了想IQKeyBoard也是全局監(jiān)聽UITextField叉抡,問題應(yīng)該不大吧~ 如果你有更好的方案,歡迎來撩~
雖然眼前問題是解決了答毫,但是此時內(nèi)心已經(jīng)暗戳戳萌芽了一個更大的困惑:dealloc方法到底干了啥褥民?
進入正題
首先,我們都知道當(dāng)一個對象的引用計數(shù)為0的時候烙常,就會調(diào)用 dealloc
方法進行析構(gòu)轴捎。在MRC時代鹤盒,內(nèi)存需要手動管理,解除對象引用需要手動調(diào) release
侦副,通常也會這樣寫 dealloc
:
- (void)dealloc {
self.instance1 = nil;
self.instance2 = nil;
// ...
// 非cocoa對象內(nèi)存的釋放侦锯,如CF對象
// ...
[super dealloc];
}
- 移除對相關(guān)實例的引用
- 非cocoa對象的釋放
- 調(diào)用
[super dealloc]
來釋放父類中的對象
而到了ARC時代,dealloc
基本變成了這樣:
- (void)dealloc {
// ...
// 非cocoa對象內(nèi)存的釋放秦驯,如CF對象
// ...
}
除了非cocoa對象還需要手動釋放尺碰,實例變量釋放和 [super dealloc]
都不見了身影。這也就是我們要探索的兩個ARC下 dealloc
的問題:
- 對象的實例變量如何釋放译隘?
- 父類中的對象析構(gòu)如何實現(xiàn)亲桥?
初探dealloc的調(diào)用
當(dāng)探索一個方法無從下手時,最好的方法就是查看調(diào)用棧固耘,說不定就能從中窺見一二题篷。測試代碼如下:
// 父類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實例很快過了作用域厅目,因此會觸發(fā)實例的釋放番枚。打印的日志如下:
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]
,也沒有手動釋放對實例變量skill的引用损敷,父類Animal的 dealloc
和實例變量skill的 dealloc
方法最終都調(diào)用了葫笼。
由于觸發(fā)對象調(diào)用dealloc的直接原因是對象引用計數(shù)為0,而實例變量實際上是被 dog.skill
這個變量所持有拗馒,因此可以通過 Watchpoint 來監(jiān)聽skill變量的內(nèi)存變化路星。在main函數(shù)的 return 0;
語句上打個斷點,然后通過 watchpoint set variable dog->_skill
設(shè)置監(jiān)聽:
繼續(xù)執(zhí)行诱桂,隨后就能監(jiān)聽到skill內(nèi)存的變化:
可見dog的skill實例變量的內(nèi)存地址從 0x00000001007661b0 變成了 0x0000000000000000洋丐,也就是說這個時間節(jié)點skill對象被釋放了(其實嚴格來說這么說是不正確的,此時堆上的skill對象并沒有被釋放访诱,我們監(jiān)聽到的只是棧上的skill變量值被清掉了垫挨,因此也就無法再通過變量訪問該對象了)韩肝。
此時調(diào)用棧如下:
可見子類的 dealloc
調(diào)用之后触菜,父類的跟著調(diào)用。隨后通過一系列運行時方法哀峻,最終在一個名為 .cxx_destruct
的方法中調(diào)用了 objc_storeStrong
來完成釋放工作涡相。另外可以看到這個 .cxx_destruct
是Animal的方法,怎么來的呢剩蟀?運行時都做了些什么事催蝗?帶著這些疑問繼續(xù)往下看。
NSObject的dealloc實現(xiàn)
是時候來看一下runtime中相關(guān)的實現(xiàn)了育特,runtime源碼可以在 Source Browser 下載丙号。
經(jīng)過定位和調(diào)用追蹤先朦,發(fā)現(xiàn)經(jīng)過了如下函數(shù):
dealloc -> _objc_rootDealloc -> object_dispose -> objc_destructInstance
前面都是些簡單的判斷和跳轉(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;
}
可以看到這個函數(shù)主要做了3件事:
-
object_cxxDestruct
這個函數(shù)有點眼熟犬缨,跟剛才調(diào)用棧中看到的
.cxx_destruct
長得很像喳魏,猜測實例變量釋放以及調(diào)用父類的dealloc
都是在這里面進行的。 -
_object_remove_assocations
顧名思義怀薛,用來釋放動態(tài)綁定的對象刺彩。
-
clearDeallocating
該函數(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(); }
可以看到做了兩件事:
- 將對象弱引用表清空,即將弱引用該對象的指針置為nil
- 清空引用計數(shù)表(當(dāng)一個對象的引用計數(shù)值過大(超過255)時枝恋,引用計數(shù)會存儲在一個叫
SideTable
的屬性中创倔,此時isa的has_sidetable_rc
值為1)
接下來,要探索的就是 object_cxxDestruct
函數(shù)了焚碌,實現(xiàn)如下:
void object_cxxDestruct(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
object_cxxDestructFromClass(obj, obj->ISA());
}
object_cxxDestructFromClass
這個函數(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í)行的其實是 SEL_cxx_destruct
這個SEL標(biāo)記的函數(shù)十电,通過全局搜索 SEL_cxx_destruct
念搬,不難發(fā)現(xiàn)該SEL對應(yīng)的正是之前看到的 .cxx_destruct 方法,也就是說摆出,最終是 .cxx_destruct
方法被調(diào)用了朗徊。
探索.cxx_destruct方法
之前在調(diào)用棧中看到該方法是Animal類中的方法,而我們并沒有申明該方法偎漫,也沒有動態(tài)插入該方法的相關(guān)代碼爷恳。并且這個方法是析構(gòu)對象相關(guān)的,具有很強的通用性象踊,那么猜測是在編譯的時候由前端編譯器(clang)自動插入的温亲。
我們可以通過 DLIntrospection 來查看Animal類中是否真的存在這個方法,該工具可以方便在lldb中打印類中所有的實例變量杯矩、方法栈虚、對象遵守的協(xié)議等信息,是一個NSObject的分類文件史隆,直接拉到工程中即可使用魂务。
在main函數(shù)中打個斷點,然后在lldb中打印Animal類的實例方法:
po [[Animal class] instanceMethods]
可以看到確實是有 .css_destruct
這個方法泌射。隨后粘姜,通過查閱相關(guān)資料,驗證了我之前的猜測熔酷。在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ù)實現(xiàn)在 CGObjC.cpp 文件中号显,其中聲明了 .cxx_destruct
的具體實現(xiàn)臭猜。最終對象釋放時,會調(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ù)做的事情是:遍歷所有實例變量获讳,調(diào)用 destroyARCStrongWithStore
。而 destroyARCStrongWithStore
最終調(diào)用的就是之前調(diào)用棧中看到的 objc_storeStrong
函數(shù)活喊,可以在runtime源碼中看到其實現(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對象賦值給location變量丐膝,因此只要執(zhí)行 objc_storeStrong(&ivar, null)
就能釋放ivar實例變量。至此钾菊,dealloc
方法如何釋放實例變量這個問題就探索完畢了帅矗。
至于如何調(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
是怎么實現(xiàn)自動釋放實例變量以及調(diào)用父類 dealloc
方法的。這一切要歸功于clang以及運行時庫料饥,在前端編譯過程中CodeGen插入了相關(guān)代碼蒲犬,結(jié)合運行時完成釋放動作。對于ARC下 dealloc
實現(xiàn)原理的摸索就此告終岸啡。
測試demo地址:https://github.com/Lotheve/blogdemo/tree/master/DeallocExporeDemo