原文 : 與佳期的個人博客(gonghonglou.com)
大家都知道葫笼,向業(yè)已回收的對象發(fā)送消息是不安全的深啤。這么做有時可以,有時不行路星。具體可行與否溯街,完全取決于對象所占內(nèi)存有沒有為其他內(nèi)容所覆寫。而這塊內(nèi)存有沒有移作他用洋丐,又無法確定呈昔,因此,應(yīng)用程序只是偶爾崩潰友绝。在沒有崩潰的情況下堤尾,那塊內(nèi)存可能只復(fù)用了其中一部分,所以對象中的某些二進(jìn)制數(shù)據(jù)依然有效迁客。還有一種可能郭宝,就是那塊內(nèi)存恰好為另外一個有效且存活的對象所占據(jù)。在這種情況下掷漱,運行期系統(tǒng)會把消息發(fā)到新對象那里粘室,而此對象也許能應(yīng)答,也許不能切威。如果能育特,那程序就不崩潰
這是《Effective Objective-C 2.0》書中”第 35 條:用“僵尸對象”調(diào)試內(nèi)存管理問題“一章中對野指針的介紹,這便是野指針出現(xiàn)的原因先朦。
本篇是 Crash 防護(hù)方案系列的第二篇文章,同樣是非常常見的 Crash 類型:EXC_BAD_ACCESS犬缨,文章會涉及以下幾點:
- 重現(xiàn) EXC_BAD_ACCESS Crash
- 分析 Xcode 中 Zoombie Objects 僵尸對象調(diào)試原理
- 自己實現(xiàn)僵尸對象調(diào)試
- EXC_BAD_ACCESS 防護(hù)
重現(xiàn) EXC_BAD_ACCESS Crash
我們先模擬一下看看野指針崩潰的樣子:
崩潰的原因是 obj 對象是用 assign 修飾的喳魏,self 并未強(qiáng)引用該對象,GHLTestObject 對象創(chuàng)建之后因為沒人引用他所以就被回收了怀薛,之后再次調(diào)用 GHLTestObject 的 log 方法則出現(xiàn)了 EXC_BAD_ACCESS 崩潰刺彩,這便是向已回收的對象發(fā)送消息產(chǎn)生的崩潰。
多說一句,這里如果把 obj 對象的 assign 修飾改成 strong创倔,則 GHLTestObject 的 log 方法可以正常執(zhí)行嗡害,因為 obj 對象被 self 強(qiáng)引用了。如果把 obj 對象的 assign 修飾改成 weak畦攘,雖然 GHLTestObject 的 log 方法不會執(zhí)行霸妹,但程序也不會崩潰,因為被 weak 修飾的指針會在對象銷毀后自動置空知押,在 OC 中向一個空對象發(fā)消息是不會崩潰的叹螟。
Xcode 中 Zoombie Objects 僵尸對象調(diào)試原理
我們開啟 Xcode 的 Zoombie Objects 選項看一下效果(Edit Scheme -> Diagnostics -> Zoombie Objects):
可以看到控制臺打印了明確的報錯信息:
2019-07-09 18:59:43.894822+0800 GHLCrashGuard_Example[51380:3729261] *** -[GHLTestObject retain]: message sent to deallocated instance 0x6000001356f0
并且能看到 self 的 obj 屬性從 GHLTestObject 類變成了 _NSZombie_GHLTestObject 類。 其實台盯,在啟用僵尸對象后罢绽,在運行期發(fā)現(xiàn) GHLTestObject 變成了僵尸對象,那么便動態(tài)的創(chuàng)建一個 _NSZombie_GHLTestObject 類静盅,將 GHLTestObject 對象的 isa 指針指向這個新的類良价,再次向 GHLTestObject 對象發(fā)消息的話就會去 _NSZombie_GHLTestObject 這個類里去找相應(yīng)的方法,然而 _NSZombie_GHLTestObject 這個類沒有實現(xiàn)任何方法蒿叠,那么發(fā)給他的全部消息都要經(jīng)過“完整的消息轉(zhuǎn)發(fā)機(jī)制”棚壁。
在發(fā)生崩潰的棧回溯消息能能看到 ___forwarding___
函數(shù)栈虚,該函數(shù)首先要做的事情就是檢查接受對象所屬的類名袖外,如果類名前綴為 _NSZombie_
,則表明消息接收者是僵尸對象魂务,那么會在控制臺打印一條消息曼验。將消息接受對象所屬的類名去掉 _NSZombie_
前綴就能得到原始類名了。
自己實現(xiàn)僵尸對象調(diào)試
有時我們可能實現(xiàn)脫離 Xcode 的僵尸對象調(diào)試粘姜,方便開發(fā)和測試的調(diào)試工走鬓照,那么可以參照 Xcode 的思路自己來實現(xiàn),即:
1孤紧、Hook NSObject 的 dealloc 方法
2豺裆、運行時動態(tài)生成新類,用 _GHLZoombie_
做前綴拼接原始類名
3号显、將僵尸對象的 isa 指針指向 _GHLZoombie_
新類
4臭猜、給 _GHLZoombie_
新類添加 forwardingTargetForSelector 方法
5、在 forwardingTargetForSelector 方法里去掉 _GHLZoombie_
前綴獲取原始類名押蚤,和調(diào)用方法名打印出來
6蔑歌、終止程序
代碼實現(xiàn):
+ (void)load {
// Bad Access
[self jr_swizzleMethod:NSSelectorFromString(@"dealloc") withMethod:@selector(zoombie_dealloc) error:nil];
}
- (void)zoombie_dealloc {
[[GHLBadAccessManager sharedInstance] handleDeallocObject:self];
}
GHLBadAccessManager 類里的處理:
NSString *GHLZoombieClassPrefix = @"_GHLZoombie_";
- (void)handleDeallocObject:(__unsafe_unretained id)object {
// 指向動態(tài)生成的類,用 _GHLZoombie_ 拼接原有類名
NSString *className = NSStringFromClass([object class]);
NSString *zombieClassName = [GHLZoombieClassPrefix 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(object, zombieClass);
}
id forwardingTargetForSelector(id object, SEL _cmd, SEL aSelector) {
NSString *className = NSStringFromClass([object class]);
NSString *realClass = [className stringByReplacingOccurrencesOfString:GHLZoombieClassPrefix withString:@""];
NSLog(@"[%@ %@] message sent to deallocated instance %@", realClass, NSStringFromSelector(aSelector), object);
abort();
}
2019-07-09 19:37:01.766612+0800 GHLCrashGuard_Example[51942:3759054] [GHLTestObject log] message sent to deallocated instance <_GHLZoombie_GHLTestObject: 0x600002b05e90>
運行程序發(fā)現(xiàn)能夠?qū)崿F(xiàn)和 Xcode 開啟僵尸對象同樣的效果
EXC_BAD_ACCESS 防護(hù)
既然我們實現(xiàn)了和 Xcode 開啟僵尸對象同樣的效果揽碘,那我們可以在最后一步不選擇終止程序次屠,而是讓程序進(jìn)入消息轉(zhuǎn)發(fā)機(jī)制园匹。
不過我們的防護(hù)方案里也可以更簡單的將原始僵尸對象的 isa 指針指向一個固定的類:GHLZoombie,不必在運行時動態(tài)的創(chuàng)建劫灶,至于獲取原始類名的問題裸违,可以通過 objc_setAssociatedObject 的方式將原始類名保存進(jìn) GHLZoombie 對象里,在 GHLZoombie 對象里重載 - (id)forwardingTargetForSelector: 方法本昏,通過 objc_getAssociatedObject 取出原始類名供汛,在控制臺打印,并將消息轉(zhuǎn)發(fā)給 GHLCrashGuardProxy 對像凛俱,在上一篇 Crash 防護(hù)方案(一):Unrecognized Selector 里講過紊馏,GHLCrashGuardProxy 對像里重載了 + (BOOL)resolveInstanceMethod: 方法避免崩潰,并收集堆棧蒲犬,上報 Crash朱监。
代碼實現(xiàn)
GHLBadAccessManager 類里的處理:
- (void)handleDeallocObject:(__unsafe_unretained id)object {
// 指向固定的類,原有類名存儲在關(guān)聯(lián)對象中
NSString *originClassName = NSStringFromClass([object class]);
objc_setAssociatedObject(object, "originClassName", originClassName, OBJC_ASSOCIATION_COPY_NONATOMIC);
object_setClass(object, [GHLZoombie class]);
}
GHLZoombie 類里的實現(xiàn):
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"[%@ %@] message sent to deallocated instance %@", objc_getAssociatedObject(self, "originClassName"), NSStringFromSelector(aSelector), self);
return [GHLCrashGuardProxy new];
}
剩下的就是上一篇文章的內(nèi)容了原叮,這樣就能做到 EXC_BAD_ACCESS Crash 的防護(hù)赫编。
但仍然存在問題是延遲釋放內(nèi)存會造成性能浪費,所以可以設(shè)置一個默認(rèn)的緩存僵尸對象的實例數(shù)量(50)或者給定一個固定內(nèi)存大蟹芰ァ(2M)擂送,超出這個限制就會釋放,當(dāng)然在釋放之后如果再此觸發(fā)了剛好釋放掉的野指針唯欣,還是會造成 Crash 的嘹吨。
Demo 地址:GHLCrashGuard:GHLCrashGuard/Classes/EXC_BAD_ACCESS
后記
小白出手,請多指教境氢。如言有誤蟀拷,還望斧正!
轉(zhuǎn)載請保留原文地址:http://gonghonglou.com/2019/07/06/crash-guard-bad-access/