Crash 防護(hù)方案(二):EXC_BAD_ACCESS

原文 : 與佳期的個人博客(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

我們先模擬一下看看野指針崩潰的樣子:


EXC_BAD_ACCESS

崩潰的原因是 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):


ZoombieObjects.png

可以看到控制臺打印了明確的報錯信息:

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

后記

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末萍聊,一起剝皮案震驚了整個濱河市问芬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌寿桨,老刑警劉巖此衅,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異亭螟,居然都是意外死亡挡鞍,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門媒佣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匕累,“玉大人,你說我怎么就攤上這事默伍』逗伲” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵也糊,是天一觀的道長炼蹦。 經(jīng)常有香客問我,道長狸剃,這世上最難降的妖魔是什么掐隐? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮钞馁,結(jié)果婚禮上虑省,老公的妹妹穿的比我還像新娘。我一直安慰自己僧凰,他們只是感情好探颈,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著训措,像睡著了一般伪节。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绩鸣,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天怀大,我揣著相機(jī)與錄音,去河邊找鬼呀闻。 笑死化借,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的捡多。 我是一名探鬼主播蓖康,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼局服!你這毒婦竟也來了钓瞭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤淫奔,失蹤者是張志新(化名)和其女友劉穎山涡,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唆迁,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡鸭丛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了唐责。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鳞溉。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖鼠哥,靈堂內(nèi)的尸體忽然破棺而出熟菲,到底是詐尸還是另有隱情看政,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布抄罕,位于F島的核電站允蚣,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏呆贿。R本人自食惡果不足惜嚷兔,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望做入。 院中可真熱鬧冒晰,春花似錦、人聲如沸竟块。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽彩郊。三九已至前弯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秫逝,已是汗流浹背恕出。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留违帆,地道東北人浙巫。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像刷后,于是被迫代替她去往敵國和親的畴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容