原文 : 與佳期的我的博客(gonghonglou.com)git
你們都知道,向業已回收的對象發送消息是不安全的。這麼作有時能夠,有時不行。具體可行與否,徹底取決於對象所佔內存有沒有爲其餘內容所覆寫。而這塊內存有沒有移做他用,又沒法肯定,所以,應用程序只是偶爾崩潰。在沒有崩潰的狀況下,那塊內存可能只複用了其中一部分,因此對象中的某些二進制數據依然有效。還有一種可能,就是那塊內存剛好爲另一個有效且存活的對象所佔據。在這種狀況下,運行期系統會把消息發到新對象那裏,而此對象也許能應答,也許不能。若是能,那程序就不崩潰github
這是《Effective Objective-C 2.0》書中」第 35 條:用「殭屍對象」調試內存管理問題「一章中對野指針的介紹,這即是野指針出現的緣由。緩存
本篇是 Crash 防禦方案系列的第二篇文章,一樣是很是常見的 Crash 類型:EXC_BAD_ACCESS,文章會涉及如下幾點:安全
咱們先模擬一下看看野指針崩潰的樣子:bash
崩潰的緣由是 obj 對象是用 assign 修飾的,self 並未強引用該對象,GHLTestObject 對象建立以後由於沒人引用他因此就被回收了,以後再次調用 GHLTestObject 的 log 方法則出現了 EXC_BAD_ACCESS 崩潰,這即是向已回收的對象發送消息產生的崩潰。函數
多說一句,這裏若是把 obj 對象的 assign 修飾改爲 strong,則 GHLTestObject 的 log 方法能夠正常執行,由於 obj 對象被 self 強引用了。若是把 obj 對象的 assign 修飾改爲 weak,雖然 GHLTestObject 的 log 方法不會執行,但程序也不會崩潰,由於被 weak 修飾的指針會在對象銷燬後自動置空,在 OC 中向一個空對象發消息是不會崩潰的。性能
咱們開啓 Xcode 的 Zoombie Objects 選項看一下效果(Edit Scheme -> Diagnostics -> Zoombie Objects): 測試
能夠看到控制檯打印了明確的報錯信息:ui
2019-07-09 18:59:43.894822+0800 GHLCrashGuard_Example[51380:3729261] *** -[GHLTestObject retain]: message sent to deallocated instance 0x6000001356f0spa
而且能看到 self 的 obj 屬性從 GHLTestObject 類變成了 _NSZombie_GHLTestObject 類。 其實,在啓用殭屍對象後,在運行期發現 GHLTestObject 變成了殭屍對象,那麼便動態的建立一個 _NSZombie_GHLTestObject 類,將 GHLTestObject 對象的 isa 指針指向這個新的類,再次向 GHLTestObject 對象發消息的話就會去 _NSZombie_GHLTestObject 這個類裏去找相應的方法,然而 _NSZombie_GHLTestObject 這個類沒有實現任何方法,那麼發給他的所有消息都要通過「完整的消息轉發機制」。 在發生崩潰的棧回溯消息能能看到 ___forwarding___
函數,該函數首先要作的事情就是檢查接受對象所屬的類名,若是類名前綴爲 _NSZombie_
,則代表消息接收者是殭屍對象,那麼會在控制檯打印一條消息。將消息接受對象所屬的類名去掉 _NSZombie_
前綴就能獲得原始類名了。
有時咱們可能實現脫離 Xcode 的殭屍對象調試,方便開發和測試的調試工走,那麼能夠參照 Xcode 的思路本身來實現,即:
一、Hook NSObject 的 dealloc 方法 二、運行時動態生成新類,用 _GHLZoombie_
作前綴拼接原始類名 三、將殭屍對象的 isa 指針指向 _GHLZoombie_
新類 四、給 _GHLZoombie_
新類添加 forwardingTargetForSelector 方法 五、在 forwardingTargetForSelector 方法裏去掉 _GHLZoombie_
前綴獲取原始類名,和調用方法名打印出來 六、終止程序
代碼實現:
+ (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 {
// 指向動態生成的類,用 _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>
運行程序發現可以實現和 Xcode 開啓殭屍對象一樣的效果
既然咱們實現了和 Xcode 開啓殭屍對象一樣的效果,那咱們能夠在最後一步不選擇終止程序,而是讓程序進入消息轉發機制。
不過咱們的防禦方案裏也能夠更簡單的將原始殭屍對象的 isa 指針指向一個固定的類:GHLZoombie,沒必要在運行時動態的建立,至於獲取原始類名的問題,能夠經過 objc_setAssociatedObject 的方式將原始類名保存進 GHLZoombie 對象裏,在 GHLZoombie 對象裏重載 - (id)forwardingTargetForSelector: 方法,經過 objc_getAssociatedObject 取出原始類名,在控制檯打印,並將消息轉發給 GHLCrashGuardProxy 對像,在上一篇 Crash 防禦方案(一):Unrecognized Selector 裏講過,GHLCrashGuardProxy 對像裏重載了 + (BOOL)resolveInstanceMethod: 方法避免崩潰,並收集堆棧,上報 Crash。
代碼實現 GHLBadAccessManager 類裏的處理:
- (void)handleDeallocObject:(__unsafe_unretained id)object {
// 指向固定的類,原有類名存儲在關聯對象中
NSString *originClassName = NSStringFromClass([object class]);
objc_setAssociatedObject(object, "originClassName", originClassName, OBJC_ASSOCIATION_COPY_NONATOMIC);
object_setClass(object, [GHLZoombie class]);
}
複製代碼
GHLZoombie 類裏的實現:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"[%@ %@] message sent to deallocated instance %@", objc_getAssociatedObject(self, "originClassName"), NSStringFromSelector(aSelector), self);
return [GHLCrashGuardProxy new];
}
複製代碼
剩下的就是上一篇文章的內容了,這樣就能作到 EXC_BAD_ACCESS Crash 的防禦。
但仍然存在問題是延遲釋放內存會形成性能浪費,因此能夠設置一個默認的緩存殭屍對象的實例數量(50)或者給定一個固定內存大小(2M),超出這個限制就會釋放,固然在釋放以後若是再此觸發了恰好釋放掉的野指針,仍是會形成 Crash 的。
Demo 地址:GHLCrashGuard:GHLCrashGuard/Classes/EXC_BAD_ACCESS
小白出手,請多指教。如言有誤,還望斧正!
轉載請保留原文地址:gonghonglou.com/2019/07/06/…