寫在前面:數(shù)組越界這類的 Crash 是最簡單的也是最容易出現(xiàn),業(yè)務(wù)開發(fā)過程中很可能操作某個 NSArray 類型的對象時忘記判空或者忘記長度判斷而造成數(shù)組越界崩潰,所以最好是在線上環(huán)境接入這類的 Crash 防護(hù)。當(dāng)然,在開發(fā)環(huán)境下最好不要接入,避免縱容開發(fā)者出現(xiàn)這類遺忘判斷的錯誤。
另外線上接入了這類的防護(hù)之后要比前邊的文章講的 Unrecognized Selector Crash 和 EXC_BAD_ACCESS Crash 更容易造成業(yè)務(wù)邏輯的錯亂,畢竟業(yè)務(wù)邏輯中不可避免的要用到大量的 NSArray、NSDictionary 類,可能在接入這類防護(hù)后會操成點擊無響應(yīng)或者頁面卡死,有時候這種情況甚至比程序崩潰還讓用戶崩潰,所以也要看實際開發(fā)需要的取舍。在接入防護(hù)后尤其要做好堆棧收集,上報 Crash 的工作,及時解決掉問題。
一、背景
- App Crash會給用戶造成很不好的用戶體驗,有時候會因為很小的問題導(dǎo)致Crash,而且有些跟業(yè)務(wù)流程無關(guān)的Crash還會阻塞業(yè)務(wù)的進(jìn)展.
- 發(fā)現(xiàn)App Crash Bug是需要我們第一時間處理的,可能周末正在LOL或者在外面陪老婆孩子,Leader一個電話我們就要第一時間回去處理
- App Crash 可能是非常小的問題造成的,但是往往會被認(rèn)定為線上嚴(yán)重問題從而對我們的績效考核造成影響(當(dāng)然最主要還是因為提升用戶體驗)
二、iOS App Crash類型
iOS App常見的Crash 類型:
- unrecognized selector crash(方法未實現(xiàn))
- Container crash(數(shù)組越界,插nil等)
- NSTimer crash
- KVO crash
- NSNotification crash
- Bad Access crash (野指針)
- UI not on Main Thread Crash (非主線程刷UI)
三、ZCZYIronMan簡介
- 目標(biāo):防護(hù)app里出現(xiàn)的前五種類型的Crash,并上報被防護(hù)住的crash
- 目前進(jìn)度:2.0版本實現(xiàn)了unrecognized selector類型的Crash防護(hù)和容器類常用API的防護(hù) NSTimer crash 的防護(hù)和KVO Crash的防護(hù) 由于iOS9之后蘋果優(yōu)化了NSNotification,所以不在對NSNotification做防護(hù) 目標(biāo)已完成目標(biāo)的90% 計劃3.0版本加上線Crash日志符號化功能(由于上線的包都是去符號的,線上獲取到的Crash調(diào)用棧信息需要符號化處理),防護(hù)代碼正在整理中后期會放到github上開源。
- 集成: 直接使用pod 'IronMan'引用項目即可不需要其他配置(當(dāng)然源碼是放在我們私有pod庫的,外部是無法使用的)
四、原理介紹
4.1 unrecognized selector防護(hù)
4.1.1.unrecognized selector Crash是怎么出現(xiàn)的
這類Crash出現(xiàn)的頻率還是比較高的,是因為對象調(diào)用沒有實現(xiàn)的方法造成的,要弄清楚這類Crash出現(xiàn)的具體原因需要對方法調(diào)用過程有一定的了解。
下面我們來看一下方法調(diào)用時Runtime大致做了些什么:
1.首先通過對象的isa指針找到對象的類Class
2.在Class的緩存方法列表中找調(diào)用的方法,如果找到,轉(zhuǎn)向相應(yīng)實現(xiàn)并執(zhí)行。
3.如果沒找到,在Class的方法列表中找調(diào)用的方法,如果找到,轉(zhuǎn)向相應(yīng)實現(xiàn)執(zhí)行
4.如果沒找到,去父類指針?biāo)赶虻膶ο笾袌?zhí)行2,3.
5.以此類推,如果一直到根類還沒找到,轉(zhuǎn)向攔截調(diào)用,走消息轉(zhuǎn)發(fā)機(jī)制。
6.如果沒有重寫攔截調(diào)用的方法,程序報錯。
4.1.2 防護(hù)方案選型
發(fā)生unrecognized selector Crash之前系統(tǒng)會給三次挽回的機(jī)會,這三次機(jī)會就在上面方法調(diào)用第5步消息轉(zhuǎn)發(fā)流程里,下面我們來了解一下消息轉(zhuǎn)發(fā)。(要先對iOS的消息機(jī)制有一定了解,才能更好理解消息轉(zhuǎn)發(fā))
消息轉(zhuǎn)發(fā)的三大步驟:消息動態(tài)解析、消息接受者重定向、消息重定向。通過這三大步驟,可以讓我們在程序找不到調(diào)用方法崩潰之前,攔截方法調(diào)用,每一步對應(yīng)一個防護(hù)方案。
大致流程如下(消息轉(zhuǎn)發(fā)詳細(xì)流程:傳送門):

1、消息動態(tài)解析:Objective-C 運行時會調(diào)用 +resolveInstanceMethod: 或者 +resolveClassMethod:,讓你有機(jī)會提供一個函數(shù)實現(xiàn)。我們可以通過重寫這兩個方法,添加其他函數(shù)實現(xiàn),并返回 YES, 那運行時系統(tǒng)就會重新啟動一次消息發(fā)送的過程。若返回 NO 或者沒有添加其他函數(shù)實現(xiàn),則進(jìn)入下一步。
2、消息接受者重定向:如果當(dāng)前對象實現(xiàn)了 forwardingTargetForSelector:,Runtime 就會調(diào)用這個方法,允許我們將消息的接受者轉(zhuǎn)發(fā)給其他對象。如果這一步方法返回 nil,則進(jìn)入下一步。
3、消息重定向:Runtime 系統(tǒng)利用 methodSignatureForSelector: 方法獲取函數(shù)的參數(shù)和返回值類型。
如果 methodSignatureForSelector: 返回了一個 NSMethodSignature 對象(函數(shù)簽名),Runtime 系統(tǒng)就會創(chuàng)建一個 NSInvocation 對象,并通過 forwardInvocation: 消息通知當(dāng)前對象,給予此次消息發(fā)送最后一次尋找 IMP 的機(jī)會。
如果 methodSignatureForSelector: 返回 nil。則 Runtime 系統(tǒng)會發(fā)出 doesNotRecognizeSelector: 消息,程序也就崩潰了
這三步都可以攔截做防護(hù)那我們怎么選擇呢
resolveInstanceMethod: 會為對象或類新增一個方法。如果此時這個類是個系統(tǒng)原生的類,比如 NSArray ,你向他發(fā)送了一條 setValue: forKey: 的方法,這本身就是一次錯發(fā)。此時如果你為他添加這個方法,這個方法一般來說就是冗余的。
forwardInvocation: 必須要經(jīng)過 methodSignatureForSelector: ** 方法來獲得一個NSInvocation,開銷比較大。蘋果在 forwardingTargetForSelector **的discussion中也說這個方法是一個相對開銷多的多的方法。
forwardingTargetForSelector: 這個方法目的單純,就是轉(zhuǎn)發(fā)給另一個對象,別的他什么都不干,相對以上兩個方法,更適合重寫。
既然** forwardingTargetForSelector: **方法能夠轉(zhuǎn)發(fā)給別其他對象,那我們可以創(chuàng)建一個類,所有的沒查找到的方法全部轉(zhuǎn)發(fā)給這個類,由他來動態(tài)的實現(xiàn)。而這個類中應(yīng)該有一個安全的實現(xiàn)方法來動態(tài)的代替原方法的實現(xiàn)。
4.1.3 最終的防護(hù)方案
防護(hù)流程:
1、對NSObject的forwardingTargetForSelector進(jìn)行hook
2、當(dāng)forwardingTargetForSelector:消息重定向觸發(fā)的時候判斷當(dāng)前類自己有沒有實現(xiàn)消息轉(zhuǎn)發(fā),如果實現(xiàn)了就走當(dāng)前類的消息轉(zhuǎn)發(fā)。
3、當(dāng)前類沒有實現(xiàn)消息轉(zhuǎn)發(fā)就動態(tài)創(chuàng)建一個類,添加當(dāng)前調(diào)用的方法,把消息轉(zhuǎn)發(fā)給這個類處理
具體實現(xiàn):
#import "NSObject+IMNIronMan.h"
#import "NSObject+IMNMethodSwizzling.h"
#import <objc/runtime.h>
@implementation NSObject (IMNIronMan)
+ (void)load {
static dispatch_once_t onceToken;
//防止重復(fù)的方法交換
dispatch_once(&onceToken, ^{
// 攔截 `+forwardingTargetForSelector:` 方法,替換自定義實現(xiàn)
[NSObject IMNIronManSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(ironMan_forwardingTargetForSelector:)
withClass:[NSObject class]];
// 攔截 `-forwardingTargetForSelector:` 方法,替換自定義實現(xiàn)
[NSObject IMNIronManSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(ironMan_forwardingTargetForSelector:)
withClass:[NSObject class]];
});
}
// 自定義實現(xiàn) `+ironMan_forwardingTargetForSelector:` 方法
+ (id)ironMan_forwardingTargetForSelector:(SEL)aSelector {
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 獲取 NSObject 的消息轉(zhuǎn)發(fā)方法
Method origin_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
// 獲取 當(dāng)前類 的消息轉(zhuǎn)發(fā)方法
Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);
// 判斷當(dāng)前類本身是否實現(xiàn)第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(origin_forwarding_method);
// 如果沒有實現(xiàn)第二步:消息接受者重定向
if (!realize) {
// 判斷有沒有實現(xiàn)第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method origin_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(origin_methodSignature_method);
// 如果沒有實現(xiàn)第三步:消息重定向
if (!realize) {
// 創(chuàng)建一個新類
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSLog(@"*** Crash Message: +[%@ %@]: unrecognized selector sent to class %p ***",errClassName, errSel, self);
NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);
// 如果類不存在 動態(tài)創(chuàng)建一個類
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注冊類
objc_registerClassPair(cls);
}
// 如果類沒有對應(yīng)的方法,則動態(tài)添加一個
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
}
// 把消息轉(zhuǎn)發(fā)到當(dāng)前動態(tài)生成類的實例對象上
return [[cls alloc] init];
}
}
return [self ironMan_forwardingTargetForSelector:aSelector];
}
// 自定義實現(xiàn) `-ironMan_forwardingTargetForSelector:` 方法
- (id)ironMan_forwardingTargetForSelector:(SEL)aSelector {
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 獲取 NSObject 的消息轉(zhuǎn)發(fā)方法
Method origin_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
// 獲取 當(dāng)前類 的消息轉(zhuǎn)發(fā)方法
Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
// 判斷當(dāng)前類本身是否實現(xiàn)第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(origin_forwarding_method);
// 如果沒有實現(xiàn)第二步:消息接受者重定向
if (!realize) {
// 判斷有沒有實現(xiàn)第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method origin_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(origin_methodSignature_method);
// 如果沒有實現(xiàn)第三步:消息重定向
if (!realize) {
//打印防護(hù)日志
logStakSymbols(self,aSelector);
// 創(chuàng)建一個新類
NSString *className = @"IronMan";
Class cls = NSClassFromString(className);
// 如果類不存在 動態(tài)創(chuàng)建一個類
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 注冊類
objc_registerClassPair(cls);
}
// 如果類沒有對應(yīng)的方法,則動態(tài)添加一個
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
}
// 把消息轉(zhuǎn)發(fā)到當(dāng)前動態(tài)生成類的實例對象上
return [[cls alloc] init];
}
}
return [self ironMan_forwardingTargetForSelector:aSelector];
}
// 動態(tài)添加的方法實現(xiàn)
static int Crash(id slf, SEL selector) {
return 0;
}
//打印調(diào)用棧信息
void logStakSymbols(id self,SEL aSelector){
NSString *selectorStr = NSStringFromSelector(aSelector);
NSLog(@"IronMan: -[%@ %@]", [self class], selectorStr);
NSLog(@"IronMan: unrecognized selector \"%@\" sent to instance: %p", selectorStr, self);
// 查看調(diào)用棧
NSLog(@"IronMan: call stack: \n%@", [NSThread callStackSymbols]);
}
@end
參考資料:
iOS 開發(fā):『Runtime』詳解(一)基礎(chǔ)知識
iOS 開發(fā):『Crash 防護(hù)系統(tǒng)』(一)Unrecognized Selector
iOS中對unrecognized selector的防御
大白健康系統(tǒng)--iOS APP運行時Crash自動修復(fù)系統(tǒng)
4.2 Container Crash防護(hù)(NSArray,NSMutableArray,NSDictionary)
4.2.1.Container Crash是什么
容器類的Crash也是比較常見的,例如:給NSMutableArray插入nil、數(shù)組越界、初始化NSDictonary時數(shù)據(jù)中有nil等。NSArray 調(diào)用addObject:方法Crash不屬于此類型,而是屬于unrecognized selector
4.2.2 防護(hù)方案選型
這種類型Crash的防護(hù)業(yè)內(nèi)常用的有兩種:
- 一種是hook常用的API,每個API中都加入
try/catch - 一種是hook常用的API,做容錯處理
第一種方法的好處是可以直接調(diào)用原來的API實現(xiàn)如果try/catch沒有捕獲到異常就不用做容錯操作,發(fā)生異常執(zhí)行容錯操作,但是壞處也很突出就是try/catch本身的開銷太大了得不償失。
第二種方法的壞處是每次都需要執(zhí)行容錯操作,但是好處是容錯操作的開銷并不會太大,可以接受
4.2.3 最終的防護(hù)方案
選中第二種方案
流程:
1、找到需要防護(hù)的容器類(由于NSArray、NSDictionary等都是類簇需要找到運行時實際的類)
2、hook常用的API,做容錯處理
下面就以NSArray舉例,其他容器類同理直接看代碼就行
/**
iOS 8:下都是__NSArrayI
iOS11: 之后分 __NSArrayI、 __NSArray0、__NSSingleObjectArrayI
iOS11之前:arr@[] 調(diào)用的是[__NSArrayI objectAtIndexed]
iOS11之后:arr@[] 調(diào)用的是[__NSArrayI objectAtIndexedSubscript]
arr為空數(shù)組
*** -[__NSArray0 objectAtIndex:]: index 12 beyond bounds for empty NSArray
arr只有一個元素
*** -[__NSSingleObjectArrayI objectAtIndex:]: index 12 beyond bounds [0 .. 0]
*/
#import "NSArray+IMNIronMan.h"
#import <objc/runtime.h>
#import "NSObject+IMNMethodSwizzling.h"
@implementation NSArray (IMNIronMan)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/**
__NSArray0 僅僅初始化后不含有元素的數(shù)組 ( NSArray *arr2 = [[NSArray alloc]init]; )
__NSSingleObjectArrayI 只有一個元素的數(shù)組 ( NSArray *arr3 = [[NSArray alloc]initWithObjects: @"1",nil]; )
__NSPlaceholderArray 占位數(shù)組 ( NSArray *arr4 = [NSArray alloc]; ) 最后會被替換成另外三個類,所以不用swizzing
__NSArrayI 初始化后的不可變數(shù)組 ( NSArray *arr1 = @[@"1",@"2"]; )
*/
// Class __NSArray = objc_getClass("NSArray");
Class __NSArrayI = objc_getClass("__NSArrayI");
Class __NSSingleObjectArrayI = objc_getClass("__NSSingleObjectArrayI");
Class __NSArray0 = objc_getClass("__NSArray0");
SEL origin_arrayWithObjects = @selector(arrayWithObjects:count:);
SEL origin_objectAtIndex = @selector(objectAtIndex:);
SEL origin_objectAtIndexedSubscript = @selector(objectAtIndexedSubscript:);
SEL my_arrayWithObjects = @selector(ironMan_arrayWithObjects:count:);
//__NSArray0
SEL my_objectAtIndexForEmptyArray = @selector(ironMan_objectAtIndexForEmptyArray:);
SEL my_objectAtIndexedForEmptyArraySubscript = @selector(ironMan_objectAtIndexedForEmptyArraySubscript:);
//__NSSingleObjectArrayI
SEL my_objectAtIndexForSingleObjectArray = @selector(ironMan_objectAtIndexForSingleObjectArray:);
SEL my_objectAtIndexedForSingleObjectArraySubscript = @selector(ironMan_objectAtIndexedForSingleObjectArraySubscript:);
//__NSArrayI
SEL my_objectAtIndex = @selector(ironMan_objectAtIndex:);
SEL my_objectAtIndexedSubscript = @selector(ironMan_objectAtIndexedSubscript:);
// 含多個object數(shù)組 arr = @[@"",@""] [arr objectAtIndex:] arr[]
[self IMNIronManSwizzlingClassMethod:origin_arrayWithObjects withMethod:my_arrayWithObjects withClass:__NSArrayI];
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndex withMethod:my_objectAtIndex withClass:__NSArrayI];
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndexedSubscript withMethod:my_objectAtIndexedSubscript withClass:__NSArrayI];
//空數(shù)組 [arr objectAtIndex:] arr[]
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndex withMethod:my_objectAtIndexForEmptyArray withClass:__NSArray0];
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndexedSubscript withMethod:my_objectAtIndexedForEmptyArraySubscript withClass:__NSArray0];
//只含一個object數(shù)組 [arr objectAtIndex:] arr[]
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndex withMethod:my_objectAtIndexForSingleObjectArray withClass:__NSSingleObjectArrayI];
[self IMNIronManSwizzlingInstanceMethod:origin_objectAtIndexedSubscript withMethod:my_objectAtIndexedForSingleObjectArraySubscript withClass:__NSSingleObjectArrayI];
});
}
+ (instancetype)ironMan_arrayWithObjects:(id _Nonnull const [])objects count:(NSUInteger)cnt{
NSUInteger newCnt = 0;
for (NSUInteger i = 0; i < cnt; i++) {
if (!objects[i]) {
break;
}
newCnt++;
}
return [self ironMan_arrayWithObjects:objects count:newCnt];
}
//__NSArray0 空數(shù)組
- (id)ironMan_objectAtIndexForEmptyArray:(NSUInteger)index{
return nil;
}
- (id)ironMan_objectAtIndexedForEmptyArraySubscript:(NSUInteger)idx{
return nil;
}
//__NSSingleObjectArrayI 只有包含一個object的數(shù)組
- (id)ironMan_objectAtIndexForSingleObjectArray:(NSUInteger)index{
if ( index >= 1) {
arrayLogStakSymbols(self,_cmd,index,1);
return nil;
}
return [self ironMan_objectAtIndexForSingleObjectArray:index];
}
- (id)ironMan_objectAtIndexedForSingleObjectArraySubscript:(NSUInteger)idx{
if (idx >= 1) {
arrayLogStakSymbols(self,_cmd,idx,1);
return nil;
}
return [self ironMan_objectAtIndexedForSingleObjectArraySubscript:idx];
}
//__NSArrayI
- (id)ironMan_objectAtIndex:(NSUInteger)index{
if ( index >= self.count) {
arrayLogStakSymbols(self,_cmd,index,self.count);
return nil;
}
return [self ironMan_objectAtIndex:index];
}
- (id)ironMan_objectAtIndexedSubscript:(NSUInteger)idx{
if (idx >= self.count) {
arrayLogStakSymbols(self,_cmd,idx,self.count);
return nil;
}
return [self ironMan_objectAtIndexedSubscript:idx];
}
//打印調(diào)用棧信息
void arrayLogStakSymbols(id self,SEL aSelector,long index,long length){
NSString *selectorStr = NSStringFromSelector(aSelector);
NSLog(@"IronMan:container Crash Bombing");
NSLog(@"IronMan: -[%@ %@]: index %ld beyond bounds [0 .. %ld]", [self class], selectorStr,index,length - 1);
// 查看調(diào)用棧
NSLog(@"IronMan: call stack: \n%@", [NSThread callStackSymbols]);
}
@end
容器類運行時實際的類型
- (void)test{
// NSArray
NSLog(@"arr alloc:%@", [NSArray alloc].class); // __NSPlaceholderArray
NSLog(@"arr init:%@", [[NSArray alloc] init].class); // __NSArray0
NSLog(@"arr:%@", [@[] class]); // __NSArray0
NSLog(@"arr:%@", [@[@1] class]); // __NSSingleObjectArrayI
NSLog(@"arr:%@", [@[@1, @2] class]); // __NSArrayI
// NSMutableArray
NSLog(@"mutA alloc:%@", [NSMutableArray alloc].class); // __NSPlaceholderArray
NSLog(@"mutA init:%@", [[NSMutableArray alloc] init].class); // __NSArrayM
NSLog(@"mutA:%@", [@[].mutableCopy class]); // __NSArrayM
NSLog(@"mutA:%@", [@[@1].mutableCopy class]); // __NSArrayM
NSLog(@"mutA:%@", [@[@1, @2].mutableCopy class]); // __NSArrayM
// NSDictionary
NSLog(@"dict alloc:%@", [NSDictionary alloc].class); // __NSPlaceholderDictionary
NSLog(@"dict init:%@", [[NSDictionary alloc] init].class); // __NSDictionary0
NSLog(@"dict:%@", [@{} class]); // __NSDictionary0
NSLog(@"dict:%@", [@{@1:@1} class]); // __NSSingleEntryDictionaryI
NSLog(@"dict:%@", [@{@1:@1, @2:@2} class]); // __NSDictionaryI
// NSMutableDictionary
NSLog(@"mutD alloc:%@", [NSMutableDictionary alloc].class); // __NSPlaceholderDictionary
NSLog(@"mutD init:%@", [[NSMutableDictionary alloc] init].class); // __NSDictionaryM
NSLog(@"mutD:%@", [@{}.mutableCopy class]); // __NSDictionaryM
NSLog(@"mutD:%@", [@{@1:@1}.mutableCopy class]); // __NSDictionaryM
NSLog(@"mutD:%@", [@{@1:@1, @2:@2}.mutableCopy class]); // __NSDictionaryM
// NSString
NSLog(@"str:%@", [@"" class]); // __NSCFConstantString
// NSNumber
NSLog(@"num:%@", [@1 class]); // __NSCFNumber
}
參考資料:
iOS崩潰處理機(jī)制:Container類型crash防護(hù)
Crash 防護(hù)方案(三):Container (NSArray、NSDictionary、NSNumber etc.)
大白健康系統(tǒng)--iOS APP運行時Crash自動修復(fù)系統(tǒng)
4.3 NSTimer Crash 防護(hù)
4.3.1 NSTimer 的問題
我們平常的開發(fā)中經(jīng)常用到NSTimer,但是NSTimer有個大坑一不小心就會遇到問題,一般我們會這樣使用NSTimer.
@interface TimerVC ()
@property(nonatomic, strong)NSTimer *timer;
@end
@implementation TimerVC
- (void)viewDidLoad {
[super viewDidLoad];
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction{
count += 1;
NSLog(@"count: %@",@(count));
}
- (void)dealloc
{
NSLog(@"%s",__func__);
[self.timer invalidate];
}
@end
聲明一個屬性持有timer,在self的dealloc里執(zhí)行invalidate,看似沒沒什問題,但是NSTimer的scheduledTimerWithTimeInterval: target: selector: userInfo:nil repeats:`會讓timer會強引用Target,而Targer又通過timer屬性持有timer,這樣就形成了循環(huán)引用,self和timer都不會被釋放,self的dealloc就不會執(zhí)行,timer會一直執(zhí)行,造成內(nèi)存泄漏,甚至在定時任務(wù)觸發(fā)時導(dǎo)致crash。 crash的展現(xiàn)形式和具體的target執(zhí)行的selector有關(guān)。
與此同時,如果NSTimer是無限重復(fù)的執(zhí)行一個任務(wù)的話,也有可能導(dǎo)致target的selector一直被重復(fù)調(diào)用且處于無效狀態(tài),對app的CPU,內(nèi)存等性能方面均是沒有必要的浪費。
4.3.2 NSTimer Crash 防護(hù)方案
解決這類Crash的關(guān)鍵就在于如何打破這個保留環(huán),網(wǎng)上流行的方案又3種
1. 在合適的時機(jī)手動釋放timer
這種方案太low了一點也不優(yōu)雅就不用過多介紹了
2.1 給NSTimer 添加一個block,把NSTimer的Target設(shè)置成timer自己,當(dāng)定時器事件觸發(fā)時調(diào)用block,這樣由于Target發(fā)生了變化,原來的保留環(huán)被打破,使得原來的Target可以正常的釋放,雖然沒有了循環(huán)引用,但是還是應(yīng)該記得在dealloc時釋放timer。
@implementation NSTimer (ActionBlock)
+ (NSTimer *)ab_scheduledTimerWithTimeInterval:(NSTimeInterval)ti block:(void(^)(void))block{
return [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(timerAction:) userInfo:[block copy] repeats:YES];
}
- (void)timerAction:(NSTimer *)timer{
void(^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
調(diào)用
_timer = [NSTimer ab_scheduledTimerWithTimeInterval:1 block:^{
NSLog(@"timerBlock");
}];
這樣確實可以打破保留環(huán),但是需要我們用使用自定義的APIab_scheduledTimerWithTimeInterval:block:老項目還得替換API,而且如果不小心調(diào)用了系統(tǒng)的API還是會有問題,還是不夠優(yōu)雅,那我們就對這個方案改進(jìn)一下.
2.2 使用Method Swizzling 配合 block
廢話不多說直接上代碼
@implementation NSTimer (ActionBlock)
+ (void)load {
static dispatch_once_t onceToken;
//防止重復(fù)的方法交換
dispatch_once(&onceToken, ^{
Method imp = class_getInstanceMethod(object_getClass([self class]), @selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:));
Method myImp = class_getInstanceMethod(object_getClass([self class]), @selector(my_scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:));
method_exchangeImplementations(imp, myImp);
});
}
+ (NSTimer *)my_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
__weak typeof(aTarget) target = aTarget;
void(^block)(void) = ^{
if ([target respondsToSelector:aSelector]) {
[target performSelector:aSelector];
}
};
return [NSTimer my_scheduledTimerWithTimeInterval:ti target:self selector:@selector(timerAction:) userInfo:[block copy] repeats:YES];
}
+ (void)timerAction:(NSTimer *)timer{
void(^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
交換系統(tǒng)API 在自定義的方法中使用block 并且使用weak調(diào)用原來target的selector 由于使用了weak不會造成循環(huán)引用,而且也可以直接使用系統(tǒng)的API,是不是很完美?但是還有一個小問題我們這里只用了userInfo來傳遞block,這樣如果需要用userInfo傳遞數(shù)據(jù)時就會有問題,下來請出第三種方案
3. 添加代理
添加一個代理IMNTimerProxy 類,用它作為NSTimer新的Target,而這個類弱引用原來的Target,通過消息轉(zhuǎn)發(fā)將timer的執(zhí)行方法轉(zhuǎn)發(fā)給原來的Target,這樣就打破了原有的循環(huán)引用。

上代碼
@implementation NSTimer (ActionBlock)
+ (void)load {
static dispatch_once_t onceToken;
//防止重復(fù)的方法交換
dispatch_once(&onceToken, ^{
Method imp = class_getInstanceMethod(object_getClass([self class]), @selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:));
Method myImp = class_getInstanceMethod(object_getClass([self class]), @selector(my_scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:));
method_exchangeImplementations(imp, myImp);
});
}
+ (NSTimer *)my_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
IMNTimerProxy *proxyObjc = [IMNTimerProxy proxyWithWeakObject:aTarget];
NSTimer * timer = [self my_scheduledTimerWithTimeInterval:ti target:proxyObjc selector:aSelector userInfo:userInfo repeats:yesOrNo];
return timer;
}
@end
設(shè)置代理對象proxyObjc為NSTimer的target
@interface IMNTimerProxy : NSObject
@property (weak, nonatomic) id weakObject;
- (instancetype)initWithWeakObject:(id)obj;
+ (instancetype)proxyWithWeakObject:(id)obj;
@end
@implementation IMNTimerProxy
- (instancetype)initWithWeakObject:(id)obj {
_weakObject = obj;
return self;
}
+ (instancetype)proxyWithWeakObject:(id)obj {
return [[IMNTimerProxy alloc] initWithWeakObject:obj];
}
/**
* 消息轉(zhuǎn)發(fā),對象轉(zhuǎn)發(fā),讓_weakObject響應(yīng)事件
*/
- (id)forwardingTargetForSelector:(SEL)aSelector {
return _weakObject;
}
@end
Proxy中弱引用obj,再通過消息轉(zhuǎn)發(fā),把timer執(zhí)行的方法轉(zhuǎn)發(fā)給原來的obj對象,這種方式解決了之前所有的問題。不過也要記得在obj的dealloc方法中釋放timer。
參考資料:
NSTimer循環(huán)引用的幾種解決方案
大白健康系統(tǒng)--iOS APP運行時Crash自動修復(fù)系統(tǒng)
4.4 KVO Crash 防護(hù)方案
4.4.1 KVO Crash 出現(xiàn)的原因
KVO API設(shè)計非常不合理,使用時一不小心就會造成Crash,此類Crash主要是因為觀察者在銷毀之后沒有移除KVO,添加KVO重復(fù)添加觀察者或重復(fù)移除觀察者(KVO 注冊觀察者與移除觀察者不匹配)導(dǎo)致的crash。
4.4.2 KVO Crash防護(hù)方案
- 有很多的KVO三方庫,比如 KVOController 用更優(yōu)的API來規(guī)避這些crash,但是侵入性比較大,必須編碼規(guī)范來約束所有人都要使用該方式。
2.像網(wǎng)易推出的大白健康系統(tǒng)
KVO的被觀察者dealloc時仍然注冊著KVO導(dǎo)致的crash 的情況,可以將NSObject的dealloc swizzle, 在object dealloc的時候自動將其對應(yīng)的kvodelegate所有和kvo相關(guān)的數(shù)據(jù)清空,然后將kvodelegate也置空。避免出現(xiàn)KVO的被觀察者dealloc時仍然注冊著KVO而產(chǎn)生的crash
這種方式也是可以的,可以完全避免KVO Crash的出現(xiàn)但是太過麻煩了。
3.可以考慮建立一個哈希表,用來保存觀察者、keyPath的信息,如果哈希表里已經(jīng)有了相關(guān)的觀察者,keyPath信息,那么繼續(xù)添加觀察者的話,就不載進(jìn)行添加,同樣移除觀察的時候,也現(xiàn)在哈希表中進(jìn)行查找,如果存在觀察者,keypath信息,那么移除,如果沒有的話就不執(zhí)行相關(guān)的移除操作。要實現(xiàn)這樣的思路就需要用到methodSwizzle來進(jìn)行方法交換。我這通過寫了一個NSObject的cagegory來進(jìn)行方法交換。
需要交換
addObserver:forKeyPath:options:context:removeObserver:forKeyPath:-
removeObserver:forKeyPath:context:
這三個方法
首先在load方法里做方法交換
@implementation NSObject (KVOCrash)
+ (void)load {
static dispatch_once_t onceToken;
//防止重復(fù)的方法交換
dispatch_once(&onceToken, ^{
SEL origin_addObserver = @selector(addObserver:forKeyPath:options:context:);
SEL origin_removeObserver = @selector(removeObserver:forKeyPath:);
SEL origin_removeObserverContext = @selector(removeObserver:forKeyPath:context:);
SEL ironMan_addObserver = @selector(ironMan_addObserver:forKeyPath:options:context:);
SEL ironMan_removeObserver = @selector(ironMan_removeObserver:forKeyPath:);
SEL ironMan_removeObserverContext = @selector(ironMan_removeObserver:forKeyPath:context:);
[NSObject IMNIronManSwizzlingClassMethod:origin_addObserver
withMethod:ironMan_addObserver
withClass:[NSObject class]];
[NSObject IMNIronManSwizzlingClassMethod:origin_removeObserver
withMethod:ironMan_removeObserver
withClass:[NSObject class]];
[NSObject IMNIronManSwizzlingClassMethod:origin_removeObserverContext
withMethod:ironMan_removeObserverContext
withClass:[NSObject class]];
});
}
//使用關(guān)聯(lián)對象創(chuàng)建hash表
- (NSHashTable *)KVOHashTable{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setKVOHashTable:(NSHashTable *)KVOHashTable{
objc_setAssociatedObject(self, @selector(KVOHashTable), KVOHashTable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
- 在自定義的
ironMan_addObserver方法里把KVO對應(yīng)的hash值存在hash表中然后調(diào)用系統(tǒng)的addObserver(由于已經(jīng)方法交換過了所以還是調(diào)用ironMan_addObserver)方法
然后再觀察者和被觀察者即將銷毀時移除對應(yīng)的kvo(這里使用了CYLDeallocBlockExecutor三方庫來監(jiān)聽對象的銷毀) - 先判斷hash表中是否保存過對應(yīng)的hashKey,如果之前添加過就不在進(jìn)行后續(xù)操作了避免重復(fù)添加
- hash表是用關(guān)聯(lián)對象保存的
- (void)ironMan_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
...省略檢測代碼
@synchronized (self) {
NSString * kvoHash = [self hashKeyWithObserver:observer keyPath:keyPath];
if (!self.KVOHashTable) {
self.KVOHashTable = [NSHashTable hashTableWithOptions:NSPointerFunctionsStrongMemory];
}
if (![self.KVOHashTable containsObject:kvoHash]) {
[self.KVOHashTable addObject:kvoHash];
[self ironMan_addObserver:observer forKeyPath:keyPath options:options context:context];
__weak typeof(observer) weakObserver = observer;
[self cyl_willDeallocWithSelfCallback:^(__unsafe_unretained id observedOwner, NSUInteger identifier) {
[observedOwner ironMan_removeObserver:weakObserver forKeyPath:keyPath context:context];
}];
__weak typeof(self) unsafeUnretainedSelf = self;
[observer cyl_willDeallocWithSelfCallback:^(__unsafe_unretained id observerOwner, NSUInteger identifier) {
[unsafeUnretainedSelf ironMan_removeObserver:observerOwner forKeyPath:keyPath context:context];
}];
}
}
}
-
ironMan_removeObserver方法在remove之前先校驗hash表里是否有KVO對應(yīng)的hash值有的話才移除,沒有的話就不移除,避免重復(fù)移除
- (void)ironMan_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
...省略校驗代碼
@synchronized (self) {
if (!observer) {
return;
}
NSString * kvoHash = [self hashKeyWithObserver:observer keyPath:keyPath];
NSHashTable *hashTable = [self KVOHashTable];
if (!hashTable) {
return;
}
if ([hashTable containsObject:kvoHash]) {
[self ironMan_removeObserver:observer forKeyPath:keyPath];
[hashTable removeObject:kvoHash];
}
}
}
- (void)ironMan_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context{
[self removeObserver:observer forKeyPath:keyPath];
}
近期整理一下代碼準(zhǔn)備上傳到github上開源,敬請期待~
參考資料:
iOS KVO crash 自修復(fù)技術(shù)實現(xiàn)與原理解析
大白健康系統(tǒng)--iOS APP運行時Crash自動修復(fù)系統(tǒng)