iOS中檢測Zoombie對象的具體實現(xiàn)
我們知道,如果在XCode中開啟了Zoombie Objects。如圖伙菜。
那么在一個對象釋放后,再次給該對象發(fā)送消息命迈,在Xcode控制臺中贩绕,可看到如下打印信息。這些信息可以幫助我們定位問題壶愤。
ZoombieDemo[12275:2841478] *** -[Test test]: message sent to deallocated instance 0x60800000b000
那么究竟XCode是如何實現(xiàn)僵尸對象的檢查的淑倾,我們將來一一揭曉。
實現(xiàn)原理
在《Effective Objective-C 》一書中有提到過僵尸指針的實現(xiàn)方式征椒。
通過hook NSObject的dealloc的方法娇哆,在一個對象要釋放的時候,通過objc_duplicateClass復(fù)制_NS_Zombie類勃救,生成_NS_Zombie_OriginaClass碍讨,并且將當(dāng)前對象的isa指向新生成的類。這塊內(nèi)存不會釋放蒙秒。
因為在給該對象發(fā)消息時勃黍,_NS_Zombie_OriginaClass并未實現(xiàn)原有類的方法,所以會走完整的消息轉(zhuǎn)發(fā)晕讲。所以我們能取出具體的OriginaClass(去掉_NS_Zombie)覆获,當(dāng)前sel榜田,打印出來。
[class seletor]:message sent to deallocated instance 0x22909"
簡單來說锻梳,就是將對象指向一個新的類,因為新類里面并沒有原有類方法的實現(xiàn)净捅,所以必定會走到消息轉(zhuǎn)發(fā)中疑枯。
以上說的是動態(tài)生成新的類,類名是通過固定前綴拼接而成蛔六,將isa指向該類荆永。其實還有一種方式,就是指向固定的類国章,原有類名通過關(guān)聯(lián)對象的方式來存儲具钥。
既然知道了原理,可以動手實現(xiàn)一下液兽。
動手實現(xiàn)
首先是hook dealloc方法骂删。在NSObject+HookDealloc中實現(xiàn)。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = NSSelectorFromString(@"dealloc");
SEL swizzledSelector = @selector(swizzledDealloc);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
動態(tài)生成新的類
在swizzledDealloc中四啰,我們通過"Zoombie_"拼接原始類名宁玫,得到一個新的類名。然后生成該類柑晒,添加
forwardingTargetForSelector的實現(xiàn)欧瘪。便于在消息轉(zhuǎn)發(fā)的時候得到調(diào)用信息。
NSString *Zoombie_Class_Prefix = @"Zoombie_";
// 指向動態(tài)生成的類匙赞,用Zoombie拼接原有類名
NSString *className = NSStringFromClass([self class]);
NSString *zombieClassName = [Zoombie_Class_Prefix stringByAppendingString: className];
Class zombieClass = NSClassFromString(zombieClassName);
if(zombieClass) return;
zombieClass = objc_allocateClassPair([NSObject class], [zombieClassName UTF8String], 0);
objc_registerClassPair(zombieClass);
class_addMethod([zombieClass class], @selector(forwardingTargetForSelector:), (IMP)forwardingTargetForSelector, "@@:@");
object_setClass(self, zombieClass);
forwardingTargetForSelector的方法實現(xiàn)佛掖,原始類名,去掉前綴即可得到涌庭。因為這里已經(jīng)是調(diào)用到已釋放對象的方法芥被,我們直接abort掉,程序?qū)⒈罎ⅰ?/p>
id forwardingTargetForSelector(id self, SEL _cmd, SEL aSelector) {
NSString *className = NSStringFromClass([self class]);
NSString *realClass = [className stringByReplacingOccurrencesOfString:Zoombie_Class_Prefix withString:@""];
NSLog(@"[%@ %@] message sent to deallocated instance %@", realClass, NSStringFromSelector(aSelector), self);
abort();
}
指向固定類
指向已有的ZoombieObject類脾猛,類名存在關(guān)聯(lián)對象中撕彤。
// 指向固定的類,原有類名存儲在關(guān)聯(lián)對象中
NSString *originClassName = NSStringFromClass([self class]);
objc_setAssociatedObject(self, "OrigClassNameKey", originClassName, OBJC_ASSOCIATION_COPY_NONATOMIC);
object_setClass(self, [ZoombieObject class]);
同上猛拴,在ZoombieObject中實現(xiàn)forwardingTargetForSelector方法羹铅,可以得到調(diào)用信息。原始類名通過關(guān)聯(lián)對象獲取愉昆。
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"[%@ %@] message sent to deallocated instance %@", objc_getAssociatedObject(self, "OrigClassNameKey"), NSStringFromSelector(aSelector), self);
abort();
}
forwardingTargetForSelector是消息轉(zhuǎn)發(fā)的第二步职员,我們也可以不在這里處理,等到最后一步forwardInvocation跛溉,不過要生成方法簽名焊切,要略微復(fù)雜些扮授。
要想走到forwardInvocation,methodSignatureForSelector返回不能是空专肪。這里我們返回了StubProxy類中stub的方法簽名(已經(jīng)定義好的類和方法)刹勃,最后就回走到forwardInvocation,通過invocation.selector可得到當(dāng)前調(diào)用方法名嚎尤。通過關(guān)聯(lián)對象獲取到原始類名荔仁。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
if (!sig) {
sig = [StubProxy instanceMethodSignatureForSelector:@selector(stub)];
}
return sig;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"[%@ %@] message sent to deallocated instance %@", objc_getAssociatedObject(self, "OrigClassNameKey"), NSStringFromSelector(anInvocation.selector), self);
}
這樣,一個簡單的檢測僵尸指針的方案就實現(xiàn)了芽死。
demo在此乏梁。
兩種方式都實現(xiàn)了,可通過調(diào)整NSObject+HookDealloc中关贵,swizzledSelector的值來切換遇骑。my_dealloc是指向動態(tài)類,swizzledDealloc是指向固定類揖曾。
SEL swizzledSelector = @selector(my_dealloc);
在App運行起來后落萎,點擊button,即可觸發(fā)翩肌。