該框架使用方法交換,將若干類的常見崩潰方法hook住瘸恼,用try...catch...捕獲Exception異常而避免崩潰奥吩。
比如對數(shù)組NSArray,針對數(shù)組越界錯誤類型构拳,交換方法objectAtIndex:為下面的實現(xiàn)
- (id)__NSArray0AvoidCrashObjectAtIndex:(NSUInteger)index {
id object = nil;
@try {
object = [self __NSArray0AvoidCrashObjectAtIndex:index];
}
@catch (NSException *exception) {
NSString *defaultToDo = AvoidCrashDefaultReturnNil;
[AvoidCrash noteErrorWithException:exception defaultToDo:defaultToDo];
}
@finally {
return object;
}
}
@try中若數(shù)組越界了則會在@catch中捕獲異常咆爽,然后處理異常梁棠。因為使用了try...catch,程序不會崩潰斗埂。
對捕獲的異常做處理
+ (void)noteErrorWithException:(NSException *)exception defaultToDo:(NSString *)defaultToDo {
//堆棧數(shù)據(jù)
NSArray *callStackSymbolsArr = [NSThread callStackSymbols];
//獲取在哪個類的哪個方法中實例化的數(shù)組 字符串格式 -[類名 方法名] 或者 +[類名 方法名]
NSString *mainCallStackSymbolMsg = [AvoidCrash getMainCallStackSymbolMessageWithCallStackSymbols:callStackSymbolsArr];
if (mainCallStackSymbolMsg == nil) {
mainCallStackSymbolMsg = @"崩潰方法定位失敗,請您查看函數(shù)調(diào)用棧來排查錯誤原因";
}
NSString *errorName = exception.name;
NSString *errorReason = exception.reason;
//errorReason 可能為 -[__NSCFConstantString avoidCrashCharacterAtIndex:]: Range or index out of bounds
//將avoidCrash去掉
errorReason = [errorReason stringByReplacingOccurrencesOfString:@"avoidCrash" withString:@""];
NSString *errorPlace = [NSString stringWithFormat:@"Error Place:%@",mainCallStackSymbolMsg];
NSString *logErrorMessage = [NSString stringWithFormat:@"\n\n%@\n\n%@\n%@\n%@\n%@",AvoidCrashSeparatorWithFlag, errorName, errorReason, errorPlace, defaultToDo];
logErrorMessage = [NSString stringWithFormat:@"%@\n\n%@\n\n",logErrorMessage,AvoidCrashSeparator];
AvoidCrashLog(@"%@",logErrorMessage);
//請忽略下面的賦值符糊,目的只是為了能順利上傳到cocoapods
logErrorMessage = logErrorMessage;
NSDictionary *errorInfoDic = @{
key_errorName : errorName,
key_errorReason : errorReason,
key_errorPlace : errorPlace,
key_defaultToDo : defaultToDo,
key_exception : exception,
key_callStackSymbols : callStackSymbolsArr
};
//將錯誤信息放在字典里,用通知的形式發(fā)送出去
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AvoidCrashNotification object:nil userInfo:errorInfoDic];
});
}
+ (NSString *)getMainCallStackSymbolMessageWithCallStackSymbols:(NSArray<NSString *> *)callStackSymbols {
//mainCallStackSymbolMsg的格式為 +[類名 方法名] 或者 -[類名 方法名]
__block NSString *mainCallStackSymbolMsg = nil;
//匹配出來的格式為 +[類名 方法名] 或者 -[類名 方法名]
NSString *regularExpStr = @"[-\\+]\\[.+\\]";
NSRegularExpression *regularExp = [[NSRegularExpression alloc] initWithPattern:regularExpStr options:NSRegularExpressionCaseInsensitive error:nil];
for (int index = 2; index < callStackSymbols.count; index++) {
NSString *callStackSymbol = callStackSymbols[index];
[regularExp enumerateMatchesInString:callStackSymbol options:NSMatchingReportProgress range:NSMakeRange(0, callStackSymbol.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
if (result) {
NSString* tempCallStackSymbolMsg = [callStackSymbol substringWithRange:result.range];
//get className
NSString *className = [tempCallStackSymbolMsg componentsSeparatedByString:@" "].firstObject;
className = [className componentsSeparatedByString:@"["].lastObject;
NSBundle *bundle = [NSBundle bundleForClass:NSClassFromString(className)];
//filter category and system class
if (![className hasSuffix:@")"] && bundle == [NSBundle mainBundle]) {
mainCallStackSymbolMsg = tempCallStackSymbolMsg;
}
*stop = YES;
}
}];
if (mainCallStackSymbolMsg.length) {
break;
}
}
return mainCallStackSymbolMsg;
}
上面呛凶,[NSThread callStackSymbols]是獲取到當前異常時的堆棧信息男娄,類型是NSArray<NSString *>,例如:
<_NSCallStackArray 0x604000047500>(
0 AvoidCrashDemo 0x0000000109abfab5 +[AvoidCrash noteErrorWithException:defaultToDo:] + 133,
1 AvoidCrashDemo 0x0000000109abb6e7 -[NSObject(AvoidCrash) avoidCrashForwardInvocation:] + 311,
2 CoreFoundation 0x000000010acc3e08 ___forwarding___ + 760,
3 CoreFoundation 0x000000010acc3a88 _CF_forwarding_prep_0 + 120,
4 AvoidCrashDemo 0x0000000109aba62a -[ViewController testNoSelectorCrash] + 106,
5 AvoidCrashDemo 0x0000000109ab8b49 -[ViewController viewDidLoad] + 73,
6 UIKit 0x000000010b3618a5 -[UIViewController loadViewIfRequired] + 1235,
7 UIKit 0x000000010b361cf2 -[UIViewController view] + 27,
8 UIKit 0x000000010b22fb83 -[UIWindow addRootViewControllerViewIfPossible] + 122,
9 UIKit 0x000000010b23028b -[UIWindow _setHidden:forced:] + 294,
10 UIKit 0x000000010b243208 -[UIWindow makeKeyAndVisible] + 42,
)
然后用正則@"[-\+]\[.+\]"匹配出數(shù)組中每一條NSString中的方法漾稀,如-[ViewController testNoSelectorCrash]模闲,得到崩潰產(chǎn)生的方法調(diào)用列表。
將處理后得到的異常信息崭捍,如原因尸折、地址等包裝成一個字典,用通知發(fā)出去殷蛇∈导校可以在AppDelegate中監(jiān)聽通知獲取異常信息,將信息上傳到自己服務器記錄粒梦。
我們一般使用的是Bugly捕獲崩潰信息亮航,但是使用AvoidCrash后異常被捕獲,程序不再崩潰匀们,Bugly不能再捕獲到塞赂,這樣對原本存在的錯誤無法知曉,萬萬不行昼蛀。
發(fā)現(xiàn)Bugly有主動上報異常接口,[Bugly reportException:exception]圆存,so叼旋,只需要將AvoidCrash框架稍微改變一點,讓最后發(fā)通知的時候?qū)⒃镜腅xception也傳遞一下沦辙,再在AppDelegate中監(jiān)聽通知夫植,調(diào)用[Bugly reportException:exception]即可。
AvoidCrash除了能防止數(shù)組越界之類的崩潰之外油讯,還能避免經(jīng)典錯誤unrecognized selector sent to instance,開啟選項后详民,在NSObject+AvoidCrash.m中,hook住消息轉(zhuǎn)發(fā)第三部的那兩個方法
// 消息轉(zhuǎn)發(fā)第三部陌兑,捕獲unrecognized selector sent to instance
- (NSMethodSignature *)avoidCrashMethodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *ms = [self avoidCrashMethodSignatureForSelector:aSelector];
if (ms == nil) {
for (NSString *classStr in noneSelClassStrings) {
if ([self isKindOfClass:NSClassFromString(classStr)]) {
ms = [AvoidCrashStubProxy instanceMethodSignatureForSelector:@selector(proxyMethod)];
break;
}
}
}
return ms;
}
- (void)avoidCrashForwardInvocation:(NSInvocation *)anInvocation {
@try {
[self avoidCrashForwardInvocation:anInvocation];
} @catch (NSException *exception) {
NSString *defaultToDo = AvoidCrashDefaultIgnore;
[AvoidCrash noteErrorWithException:exception defaultToDo:defaultToDo];
} @finally {
}
}
使用AvoidCrash注意點:
1沈跨、使用[AvoidCrash setupNoneSelClassStringsArr:]]方法時,參數(shù)數(shù)組不能中不能有NSObject兔综,否則會有些奇奇怪怪的bug饿凛,比如彈出鍵盤時會崩潰
2狞玛、數(shù)組中缺少一組交換方法,對象是__NSArrayM涧窒,方法是objectAtIndexedSubscript:
附錄:
16年5月份的時候心肪,那時剛接手別人一個藏家項目,崩潰太多了纠吴,集成Bugly一個版本硬鞍,發(fā)現(xiàn)大多崩潰原因都是諸如數(shù)組越界、數(shù)組字典添加空對象戴已、字符串截取子串NSRange越界等等固该,崩潰之多令人無言以對,逐個排查太費時間恭陡,遂干脆偷懶針對所有的此類bug寫分類蹬音,原理跟AvoidCrash差不多,不過沒有做Exception處理休玩,僅僅是防止崩潰而已著淆。
實現(xiàn)如數(shù)組分類
@implementation NSArray (Exception)
+(void)load{
SEL originalSelector = @selector(objectAtIndex:);
SEL swizzledSelector = @selector(by_objectAtIndex:);
Method originalMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), originalSelector);
Method swizzledMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (id)by_objectAtIndex:(NSUInteger)index{
if (self.count == 0) {
NSLog(@"%s----數(shù)組為空",__FUNCTION__);
return nil;
}
if (index > self.count - 1) { // 防止數(shù)組越界崩潰
NSLog(@"%s----數(shù)組越界",__FUNCTION__);
return nil;
}
return [self by_objectAtIndex:index];
}
@end
我看了AvoidCrash第一個版本是16年10月,也許思路還借鑒了我的哈拴疤。