前言
軟件在運行時遇到不能理解的異常時會中斷執(zhí)行,iOS系統(tǒng)下給出的解決方案是強制結(jié)束應(yīng)用并回到桌面,該方案不僅丟失內(nèi)存信息,還會阻斷用戶操作流程,對業(yè)務(wù)影響極其嚴(yán)重,所以線上應(yīng)用當(dāng)極力避免此類情況,開發(fā)過程中應(yīng)當(dāng)對iOS系統(tǒng)下會引發(fā)崩潰的所有點牢記在心。對于無法預(yù)料的編碼bug,可通過多種方式接管系統(tǒng)異常,以更溫和的、低成本的彈窗或提醒等方案保證用戶的其它正常操作。等后續(xù)通過排查線上日志定位異常后,再進行修改。
系統(tǒng)崩潰事件
- 方法未找到(類或者實例執(zhí)行方法出錯)
- 空值nil (數(shù)組插入nil,setObject值為nil)
- 下標(biāo)越界(數(shù)組操作,字符串操作)
- 野指針 (assign修飾非基本數(shù)據(jù),和CF框架交互釋放錯誤)
- KVO觀察者未釋放 (持有和釋放未一一對應(yīng))
- NAN錯誤
- 線程異常 (子線程操作UI)
- 內(nèi)存暴漲,超出系統(tǒng)閾值 (內(nèi)存泄漏,遞歸調(diào)用等)
對應(yīng)接管方案
1. iOS系統(tǒng)的方法查找流程

可以看到消息轉(zhuǎn)發(fā)階段有三個接管時機,具體該在哪個時機接管,各有優(yōu)勢,不再贅述。
此類異常的接管可通過重寫消息轉(zhuǎn)發(fā)方法來實現(xiàn):
- NSObject下增加分類,用于全局替換
- 新增方法 swizze_xxx 與系統(tǒng)轉(zhuǎn)發(fā)方法 xxx 方法交換 (xxx 為消息轉(zhuǎn)發(fā)三個時機中的方法)
- 在新方法中跳過崩潰后,上報異常到服務(wù)器。
2.空值&下標(biāo)越界
空值
先補充下各種空值的意義:
- nil
nil具體指的是oc下實例的空指針,nil 調(diào)用方法并不會發(fā)生異常,但因為nil在容器類中有特殊含義(NSArray,NSDictionary中nil代表結(jié)束位),所以使用 addObject 或setObject時 參數(shù)為nil時會報被系統(tǒng)當(dāng)做嚴(yán)重異常。 - Nil
類對象的空指針,類對象在OC中是以單例存在的,所以基本不會遇到,無需考慮。 - Null
C類型的空指針,暫不考慮 - NSNull
OC下的標(biāo)準(zhǔn)空值類型,常見的場景就是與服務(wù)器數(shù)據(jù)交互中,模型中對象類型的屬性如果遇到空值就會被重置成NSNull類型,此時如果不注意會出現(xiàn)很多 unrecognized selector 的崩潰,因為對象類型已經(jīng)變了。
空值涉及的類有很多,主要包含各種可變?nèi)萜鞯目罩挡迦耄鉀Q方案為將所有的插入空值會引起崩潰的方法全部替換為新方法。
//NSArray
- (void) addObject:(id)anObject;
- (void) insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void) replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;
- (void) setObject:(id)object atIndexedSubscript:(NSUInteger)index;
//NSDictionary
+ (instancetype) dictionaryWithObject:(id)object forKey:(id)key;
- (void) setObject:(id)anObject forKey:(id)aKey;
- (void) setObject:(id)object forKeyedSubscript:(id<NSCopying>)key;
//NSSet
+ (instancetype)setWithObject:(id)object;
- (void) addObject:(id)object;
- (void) removeObject:(id)object;
- 新建類并重寫 + load 方法。 (+load 方法優(yōu)先于main函數(shù)且只執(zhí)行一次),或者可以保證整個App生命周期只調(diào)用一次即可。
- 新增對應(yīng)hook方法。
- 用runtime 的method swizzing API 進行方法交換。
例如以下:
swizzleInstanceMethod([NSArray class], @selector(addObject:), @selector(hookAddObject:));
- (void) hookAddObject:(id)objc {
if (objc) {
[self hookAddObject];
}
handleCrashException(@"hookAddObject object is nil");
}
#pragma mark - 方法交換
- (void)swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
下標(biāo)越界
下標(biāo)越界涉及眾多類和API,涉及數(shù)組操作,字符串操作,UITableViewCell的添加刪除等等。
好在原理一致,替換對應(yīng)的方法后在內(nèi)部做判斷即可。
3.野指針、僵尸對象
野指針是對象釋放過程出現(xiàn)了錯誤,導(dǎo)致對象失去了控制權(quán),此時的對象就成了僵尸對象,其對應(yīng)的指針便稱作野指針,當(dāng)繼續(xù)操作該指針時,其所指向的內(nèi)存地址可能已經(jīng)被分配給了其它對象,所以在此時時會產(chǎn)生很多意想不到的錯誤。
一個典型的引發(fā)野指針的場景就是: 非基本類型的對象用 assign修飾 (常見為delegate屬性修飾),因為assign屬性的對象不會隨著對象釋放而釋放。常見的判斷野指針的方式為:查看日志中是否頻繁出現(xiàn)某個類的實例調(diào)用了一堆不相關(guān)的方法。

解決方案:
在ARC模式下,野指針出現(xiàn)的場景已經(jīng)很少了,非基本類型的對象用assign修飾在編譯器就會報錯,所以基本只要注意 id類型的屬性不要用 assign修飾即可。
野指針定位很困難,因為僵尸對象何時被分配怎樣被分配是很隨機的,導(dǎo)致野指針的崩潰完全成了人品問題。
網(wǎng)上比較高階的解決方案主要是在開發(fā)階段將野指針暴露出來,盡力提高野指針復(fù)現(xiàn)場景,其原理為:hook dealloc 方法,阻止對象正常釋放(內(nèi)存使用會飆升),同時將對應(yīng)內(nèi)存地址重寫為0x55(不可操作),當(dāng)野指針再次操作僵尸對象時,因為對應(yīng)內(nèi)存已不可操作就會發(fā)生崩潰,再通過一些更底層的函數(shù),獲取崩潰時調(diào)用的堆棧信息,定位到野指針調(diào)用的類名和方法,繼而通過改代碼解決問題。
過程很復(fù)雜,探究的精神很是佩服,但即便這樣仍舊不能完全解決問題,首先工作方式會有一些限制,比如內(nèi)存飆升的問題,解決方式為當(dāng)占用快滿時主動釋放掉一些內(nèi)存,然后讓系統(tǒng)重新分配,但屬于該片內(nèi)存的野指針就會被漏診,所以內(nèi)存越大的設(shè)備加操作更可能出現(xiàn)的業(yè)務(wù)場景,才能有更多的幾率找到野指針。
4.KVO、NSNotificationCenter
- KVO原理
2.如何引發(fā)?為什么會系統(tǒng)被認(rèn)作是嚴(yán)重錯誤?
KVO、通知,造成崩潰的一個共性原因就是持有對象未釋放,但看起來這就是普通的內(nèi)存泄漏問題,為什么會被系統(tǒng)認(rèn)為是嚴(yán)重錯誤,為什么循環(huán)引用就不認(rèn)為是嚴(yán)重錯誤呢?
3.如何解決
先明確幾個概念,用以下代碼舉例:
Person *per = [[Person alloc] init];
[per addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
per.name = @"zhangsan";
1.觀察者observer,也就是其中的self,當(dāng)變化發(fā)生時負(fù)責(zé)接收回調(diào)事件。
2.被監(jiān)聽者per,調(diào)用addObserver后自身遍和觀察者產(chǎn)生了關(guān)聯(lián),當(dāng)觀察者dealloc時,如果 per 沒有調(diào)用過 removeObserver ,就會發(fā)生崩潰。
3.鍵值keypath,此處為number屬性,監(jiān)聽后再修改per.name就會通知到觀察者self,當(dāng)keypath已經(jīng)被添加過,或者removeObserverFromKeypath時移除了多次都會發(fā)生崩潰。
這里吐槽下KVO的設(shè)計,要是局部變量想監(jiān)聽下屬性變化必須得改成全局變量,要不然都沒法在dealloc中寫移除觀察者的代碼,這不是很坑嘛,其次一個鍵值被監(jiān)聽后再次添加觀察者會崩潰,移除次數(shù)不對也會崩潰。那你好歹告知下這個鍵有沒有被監(jiān)聽才是啊,提供個相關(guān)屬性多好(例如isObserved?),現(xiàn)在好了,搞幾個大坑出來,要想正常使用就必須封裝下搞兩個容器看對象的某個keypath是不是已經(jīng)被監(jiān)聽過,觀察者釋放還后要看被監(jiān)聽者是不是被移除了....
推薦個靠譜封裝 https://github.com/facebook/KVOController
主要原理為:
- 建立兩個容器,一個放置keypath對象,用于判斷其添加和移除次數(shù)是不是一一對應(yīng)。另外一個放置被監(jiān)聽對象,用于觀察者對象釋放時,移除對應(yīng)的監(jiān)聽對象。
- 新建類xxx創(chuàng)建實例作為觀察者的運行時的associated對象,重寫其dealloc方法,這樣當(dāng)觀察者對象時,就會執(zhí)行dealloc,監(jiān)聽對象的移除操作放到此處即可。
完美。
通知崩潰在iOS9以上已經(jīng)沒有了,也不過多討論,再說通知本就已經(jīng)是單例在全局操作觀察者,比KVO更好處理崩潰問題,為毛還要開發(fā)者手動移除呢,可能我們作為開發(fā)者尤其重視崩潰而蘋果作為語言設(shè)計者根本就不在乎吧。
JJException框架內(nèi)部對于KVO保護的原理頗為復(fù)雜,為了實現(xiàn)無感知保護,hook了系統(tǒng)KVO的三個方法
+ (void)jj_swizzleKVOCrash{
swizzleInstanceMethod([self class], @selector(addObserver:forKeyPath:options:context:), @selector(hookAddObserver:forKeyPath:options:context:));
swizzleInstanceMethod([self class], @selector(removeObserver:forKeyPath:), @selector(hookRemoveObserver:forKeyPath:));
swizzleInstanceMethod([self class], @selector(removeObserver:forKeyPath:context:), @selector(hookRemoveObserver:forKeyPath:context:));
}
內(nèi)部原理和KVOController框架實現(xiàn)基本一致,但有個弊端是對某些使用系統(tǒng)KVO的第三方有影響,因為內(nèi)部hook了dealloc方法主動執(zhí)行了removeObserver操作,無需外部再調(diào)用,但第三方的remove操作無法修改,App運行時總是停在@try內(nèi)部,日常調(diào)試很受影響。

5.NaN錯誤
這個沒有一勞永逸的方案,特別注意算數(shù)計算中 除數(shù)不能為0
另外可以通過isnan(x)函數(shù)來判斷。
6.線程異常
子線程操作UI
線程鎖問題
7.內(nèi)存泄漏
- NSTimer 操作不當(dāng)導(dǎo)致持續(xù)運行
NSTimer 如果初始化為局部變量,那么和target容易造成循環(huán)引用,導(dǎo)致timer不能釋放,對應(yīng)的selector不斷執(zhí)行,很可能引發(fā)內(nèi)存暴漲。 - 循環(huán)引用導(dǎo)致的內(nèi)存泄漏