原文連接html
當所指向的對象被釋放或者收回,可是對該指針沒有做任何的修改,以致於該指針仍舊指向已經回收的內存地址,此狀況下該指針便稱野指針git
野指針異常堪稱crash界
的半壁江山,相比起NSException
而言,野指針有這麼兩個特色:github
隨機性強
web
儘管大公司已經有各類單元、行爲、自動化以及人工化測試,儘可能的去模擬用戶的使用場景,但野指針異常老是能巧妙的避開測試,在線上大發神威。緣由毫不僅僅在於測試沒法覆蓋全部的使用場景算法
形成野指針是多樣化的:首先內存被釋放後不表明內存會馬上被覆寫
或者數據受到破壞
,這時候訪問這塊內存也不必定會出錯。其次,多線程技術
帶來了複雜的應用運行環境,在這個環境下,未加保護的數據多是致命的。此外,設計不夠嚴謹的代碼
一樣也是形成野指針異常的重要緣由之一安全
難以定位
多線程
NSException
是高抽象層級上的封裝,這意味着它能夠提供更多的錯誤信息給咱們參考。而野指針幾乎出自於C語言
層面,每每咱們能得到的只有系統棧信息,單單是定位錯誤代碼位置已經很難了,更不要說去重現修復架構
解決野指針最大的難點在於定位。一般線上出現了crash
須要修復時,開發者最重要的一個步驟是重現crash
。而上文提到了野指針的兩個特性會阻礙咱們定位問題,對於這兩個特性,確實也能作一些對應的處理來下降它們的干擾性:ide
採集輔助信息
函數
輔助信息包括設備信息、用戶行爲等信息,每每能夠用來重現問題。好比用戶行爲能夠造成用戶使用路徑
,從而重現用戶使用場景。而在發生crash
時,採集當前頁面信息,配合用戶使用路徑
能夠快速的定位到問題發生的大概位置。通過驗證,輔助信息
確實有效的減小了系統棧
對於問題重現的干擾
提升野指針崩潰率
因爲野指針不必定會發生崩潰這一特性,即使咱們經過堆棧信息
和輔助信息
肯定了大體範圍,不表明咱們能順利的重現crash
。一個優秀的野指針崩潰能夠形成一天開發,三天debug
,假如野指針的崩潰不是隨機的,那麼問題就簡單的多
Xcode
提供了Malloc Scribble
對已釋放內存進行數據填充,從而保證野指針訪問是必然崩潰的。另外,Bugly
借鑑這一原理,經過修改free
函數,對已釋放對象進行非法數據填充,也有效的提升了野指針的崩潰率
Zombie Objects
Zombie Objects
是一種徹底不一樣的野指針調試機制,將釋放的對象標記爲Zombie
對象,再次給Zombie
對象發送消息時,發生crash
而且輸出相關的調用信息。這套機制同時定位了發生crash
的類對象以及有相對清晰的調用棧
整理一下上述的內容,能夠看到目前存在輔助信息+對象內存填充
以及Zombie Objects
這兩種主要的應對方式。拿前者來講,填充已釋放對象的內存風險高,通過嘗試Xcode9
的Malloc Scribble
啓動後已經不會填充對象的內存地址。其次,填充內存須要去hook
更加底層的API
,這意味着對代碼能力要求更高。所以,借鑑Zombie Objects
的實現思路去定位野指針異常是一個可行的方案
轉發是一項有趣的機制,它經過在通訊雙方中間,插入一箇中間層。發送方再也不耦合接收方,它只須要將數據發送給中間層,由中間層來派發給具體的接收方。基於轉發的思想,能夠作許多有趣的東西:
消息轉發
iOS
的消息機制讓咱們能夠給對象發送一個未註冊的消息,一般這會引起unrecognized selector
異常。可是在拋出異常以前,存在一個消息轉發
機制,容許咱們從新指定消息的接收方來處理這個消息。正是這一機制實現了防unrecognized selector crash
的可行化
打破引用環
循環引用是ARC
環境下最容易出現的內存問題,當多個對象之間的引用造成了引用環時,極有可能會致使環中的對象都沒法被釋放。借鑑Proxy
的方式,能夠實現破壞引用環的做用。XXShield以插入WeakProxy
層的方式實現了防crash
路由轉發
組件化是項目體量達到必定程度時必須考慮的架構方案,將項目拆分基礎組件和業務組件,加入中間層實現組件間解耦的效果。因爲業務組件之間互不依賴,所以須要合適的方案實現組件通訊,路由設計是一種經常使用的通訊方式。各個模塊實現canOpenURL:
接口來判斷是否處理對應的跳轉邏輯,模塊將參數信息拼接在url
中傳遞:
都說消息發送
是Objective-C
的核心機制,任何一個對象方法調用都會被轉換成objc_msgSend
的方式執行。這一過程當中涉及到一個重要的變量:isa
指針。多數開發者對isa
指針停留在它指向了類的類結構自己的地址,用來表示對象的類型。可是實際上isa
指針要比咱們想一想的複雜的多,好比objc_msgSend
依賴於isa
來完成消息的查找,經過閱讀經過彙編解讀 objc_msgSend能夠了解更詳細的匹配過程:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
複製代碼
因爲方法調用與isa
指針相關,所以若是咱們修改一個類的isa
指針使其指向一個目標類,那麼能夠實現對象方法調用的攔截,也能夠稱做對象方法轉發。咱們並不能直接修改isa
指針,但runtime
提供了一個object_setclass
接口容許咱們動態的對某個類進行重定位
ClassA
被重定位成ClassB
須要保證兩個類的內存結構是對齊的,不然可能會發生超出意外的問題
通常來講咱們都不該該違背重定位類的內存結構對齊原則。但在野指針問題中,對象擁有的內存被釋放後是不肯定狀態,所以作適當的破壞
並不必定是壞事,只是記住在最終釋放對象內存時,應當再次重定位回來,防止內存泄漏的風險
借鑑於Zombie Objects
的機制,咱們能夠實現一套類Zombie Proxy
機制。經過重定位類型
的作法,在對象dealloc
以前將其isa
指針指向一個目標類,實現後續調用的轉發。而目標類中全部的方法調用都採用NSException
的機制拋出異常,而且輸出調用對象的實際類型和調用方法幫助定位:
重定位後的類因爲其實際用於轉發的用途,更符合Proxy
的屬性,所以我將其設置爲NSProxy
的子類,多數人可能不知道iOS
一共有NSProxy
跟NSObject
兩個根類。另外,爲了實現對retain
等內存管理相關方法的重寫,目標類應該設置爲不支持ARC
:
@interface LXDZombieProxy : NSProxy
@property (nonatomic, assign) Class originClass;
@end
@implementation LXDZombieProxy
- (void)_throwMessageSentExceptionWithSelector: (SEL)selector
{
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"(-[%@ %@]) was sent to a zombie object at address: %p", NSStringFromClass(self.originClass), NSStringFromSelector(selector), self]
userInfo:nil];
}
#define LXDZombieThrowMesssageSentException() [self _throwMessageSentExceptionWithSelector: _cmd]
- (id)retain
{
LXDZombieThrowMesssageSentException();
return nil;
}
- (oneway void)release
{
LXDZombieThrowMesssageSentException();
}
- (id)autorelease
{
LXDZombieThrowMesssageSentException();
return nil;
}
- (void)dealloc
{
LXDZombieThrowMesssageSentException();
[super dealloc];
}
- (NSUInteger)retainCount
{
LXDZombieThrowMesssageSentException();
return 0;
}
@end
複製代碼
因爲iOS
的方法其實是以向上調用
的鏈式機制實現的,所以只須要hook
掉兩個根類的dealloc
方法就能保證對對象類型的重定位。在hook
掉dealloc
以後有幾個須要注意的點:
對象的釋放
因爲咱們須要實現轉發機制,這表明着本該釋放的對象在類型重定位後不能被釋放。隨着時候時間的推移,重定位類對象的數量會愈來愈多。根據經驗來講,通常的野指針在30s
內被再次訪問的機率很大,所以咱們能夠在類型重定位完成後延後30s
釋放對象。或者能夠構建一個Zombie Pool
,當內存佔用達到必定大小時,使用恰當的算法淘汰
白名單機制
並非全部的類對象都被監控,好比系統私有類
、監控相關工具類
、明確不存在野指針的類
等。咱們須要一個全局的白名單系統,來確保這些類的dealloc
是正常執行的,無需被轉發
潛在的crash
經過method_setImplementation
替換dealloc
的代碼實現,因爲我採用block
轉IMP
的方式來實現的方式,會對捕獲的外界對象進行引用。而對象在重定位後,任何調用都會引起crash
,所以須要針對這種狀況作對應的處理
爲了知足保證對象可以在達成釋放條件完成內存的回收,須要存儲根類的dealloc
原實現,以根類類名做爲key
存儲在全局字典中。而且提供接口__lxd_dealloc
來完成對象的釋放工做:
static inline void __lxd_dealloc(__unsafe_unretained id obj) {
Class currentCls = [obj class];
Class rootCls = currentCls;
while (rootCls != [NSObject class] && rootCls != [NSProxy class]) {
rootCls = class_getSuperclass(rootCls);
}
NSString *clsName = NSStringFromClass(rootCls);
LXDDeallocPointer deallocImp = NULL;
[[_rootClassDeallocImps objectForKey: clsName] getValue: &deallocImp];
if (deallocImp != NULL) {
deallocImp(obj);
}
}
NSMutableDictionary *deallocImps = [NSMutableDictionary dictionary];
for (Class rootClass in _rootClasses) {
IMP originalDeallocImp = __lxd_swizzleMethodWithBlock(class_getInstanceMethod(rootClass, @selector(dealloc)), swizzledDeallocBlock);
[deallocImps setObject: [NSValue valueWithBytes: &originalDeallocImp objCType: @encode(typeof(IMP))] forKey: NSStringFromClass(rootClass)];
}
複製代碼
在對象的dealloc
被調起以後,檢測對象類型是否存在白名單中。若是存在,直接繼續完成對對象的釋放工做。不然的話,延後30s
進行釋放工做。爲了解除block
引用形成的crash
,使用NSValue
存儲對象信息以及使用__unsafe_unretained
來防止臨時變量的引用:
swizzledDeallocBlock = [^void(id obj) {
Class currentClass = [obj class];
NSString *clsName = NSStringFromClass(currentClass);
/// 若是爲白名單,則不重定位類的類型
if ([__lxd_sniff_white_list() containsObject: clsName]) {
__lxd_dealloc(obj);
} else {
NSValue *objVal = [NSValue valueWithBytes: &obj objCType: @encode(typeof(obj))];
object_setClass(obj, [LXDZombieProxy class]);
((LXDZombieProxy *)obj).originClass = currentClass;
/// 延後30秒釋放對象,避免形成內存的浪費
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__unsafe_unretained id deallocObj = nil;
[objVal getValue: &deallocObj];
object_setClass(deallocObj, currentClass);
__lxd_dealloc(deallocObj);
});
}
} copy];
複製代碼
具體的實現代碼能夠下載LXDZombieSniffer
野指針問題是訪問了非法內存致使的crash
,也就是說要符合兩個條件:內存非法
以及指針地址不爲NULL
。在iOS
中存在三種不一樣修飾的指針:
__strong
默認修飾符。修飾的指針在賦值以後,會對指向的對象執行一次retain
操做,指針不因對象的生命週期變化而改變
__unsafed_unretained
非安全對象指針修飾符。修飾的指針不會持有指向對象,也不因對象的生命週期發生變化而改變,等同於assign
__weak
弱對象指針修飾符。修飾的指針不會持有指向對象,在對象的生命週期結束而且內存被回收時,修飾的指針內容會被重置爲nil
根據野指針異常的引起條件來講,三種修飾指針只有__strong
和__unsafed_unretained
能夠致使野指針訪問異常。可是在使用類別重定位
以後,本該釋放的對象會被延時或者不釋放,也就是本該被重置的弱指針也不會發生重置,這時使用弱指針訪問對象應該會被轉發到ZombieProxy
當中發生crash
:
__weak id weakObj = nil;
@autoreleasepool {
NSObject *obj = [NSObject new];
weakObj = obj;
}
/// The operate should be crashed
NSLog(@"%@", weakObj);
複製代碼
然而在上面的測試中,發現即使對象被重定位爲Zombie
而且被阻止釋放以後,weakObj
依舊被成功的設置成了nil
。而後通過objc_runtime源碼運行和添加斷點測試以後,也沒有weak
指針被重置的調用。甚至使用了LLVM
的watch set var weakObj
監控弱指針,依舊沒法找到調用。但weakObj
在dealloc
調用以後,無論對象有沒有被釋放,都被重置成了nil
。這也是截止文章出來爲止,匪夷所思的疑難雜症