線上的崩潰對于APP來說是致命性的,但應用崩潰的原因有很多,比如方法找不到,數組越界等等
先解決 方法找不到 unrecognized selector sent to instance 而產生的崩潰吧。
一、實現原理
說到實現原理,首先需要了解一個問題:一個對象收到一個無法響應的方法到程序崩潰,這之間發生了什么?
答:當一個方法找不到的時候,會走攔截調用和消息轉發流程。
大致流程是:
1、對象調用方法時,系統會先調用該類的 +resolveInstanceMethod: 方法進行判斷,如果返回 YES, 則表示能接受消息,NO 表示不能接受消息并進入第下步。
2、系統會調用該類的 -forwardingTargetForSelector:,在這個方法中,可以把這個消息轉發給其他對象。敲黑板,這里是重點。
3、如果第二部返回 nil,那么首先它會發送 -methodSignatureForSelector: 消息,獲得函數的參數和返回值類型。如果 -methodSignatureForSelector: 返回nil,Runtime則會發出 -doesNotRecognizeSelector: 消息,程序這時也就掛掉了。如果返回了一個函數簽名,Runtime 就會創建一個 NSInvocation 對象并發送 -forwardInvocation:消息給目標對象。
二、實現
1、給 NSObject 添加一個分類,再在這個分類中自定義一個方法,-my_forwardingTargetForSelector:,在 +load 方法中,交換 NSObject 的 forwardingTargetForSelector: 和 my_forwardingTargetForSelector:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod([NSObject class], @selector(forwardingTargetForSelector:));
Method swizzledMethod = class_getInstanceMethod([NSObject class], @selector(my_forwardingTargetForSelector:));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
2、在自定義的方法中,先判斷當前對象是否已經實現了消息轉發方法,如果沒有實現,就動態創建一個類,給這個類動態添加一個方法,再把消息轉發給這個動態創建的方法,這樣就不會崩潰了。
- (id)my_forwardingTargetForSelector:(SEL)aSelector {
// 獲取NSObject的消息轉發方法
SEL sel = NSSelectorFromString(@"forwardingTargetForSelector:");
Method method = class_getInstanceMethod(NSClassFromString(@"NSObject"), sel);
// 獲取當前類的消息轉發方法
Method _m = class_getInstanceMethod([self class],sel);
// 類本身有沒有實現消息轉發流程
BOOL transmit = method_getImplementation(_m) == method_getImplementation(method);
// 有木有實現下一步消息轉發流程
if (transmit) {
// 判斷有沒有實現第三步消息轉發
SEL sel1 = NSSelectorFromString(@"methodSignatureForSelector:");
Method method1 = class_getInstanceMethod(NSClassFromString(@"NSObject"), sel1);
Method _m1 = class_getInstanceMethod([self class], sel1);
transmit = method_getImplementation(_m1) == method_getImplementation(method1);
if (transmit) {
// 創建一個新類
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSLog(@"出問題的類,出問題的方法 == %@ %@", errClassName, errSel);
NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);
/// 如果類不存在 動態創建一個類
if (!cls) {
Class superCls = [NSObject class];
cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
/// 給類添加方法
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
objc_registerClassPair(cls);
}
/// 如果類沒有對應的方法,則動態添加一個
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
}
/// 把消息轉發到當前動態生成類的實例上
return [[NSClassFromString(className) alloc] init];
}
}
return [self my_forwardingTargetForSelector:aSelector];
}
static int Crash(id slf, SEL selector) {
return 0;
}
主要是注意兩點:
1:類有沒有實現 forwardingTargetForSelector: 函數;
2:類有沒有實現 methodSignatureForSelector: 函數,實現了就表明類實現了消息轉發流程,那么不處理這個類。
三、其他類避免崩潰
還有一個常見的崩潰就是數組越界 index 2 beyond bounds [0 .. 1],對于這一類方法,方法是把系統的方法和自定義的方法交換,在自定義的方法里判斷是否越界。
根據上面的思路,給 NSArray 添加分類,交換 NSArray 的 objectAtIndex: 方法。
然鵝,但是,然并暖,程序還是崩潰了,信息如下:
Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 2 beyond bounds [0 .. 1]'
仔細查看崩潰日志,終于發現問題了:-[__NSArrayI objectAtIndexedSubscript:]。這里是調用了 __NSArrayI 這個類的 -objectAtIndexedSubscript: 方法,而不是 NSArray 的方法。再來一次,具體代碼如下:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//不可變數組
Class __NSArrayI = NSClassFromString(@"__NSArrayI");
Method originalMethod = class_getInstanceMethod(__NSArrayI, @selector(objectAtIndexedSubscript:));
Method swizzledMethod = class_getInstanceMethod(__NSArrayI, @selector(my_objectAtIndexedSubscript:));
method_exchangeImplementations(originalMethod, swizzledMethod);
//可變數組
Class __NSArrayM = NSClassFromString(@"__NSArrayM");
Method oldMuTableMethod = class_getInstanceMethod(__NSArrayM, @selector(objectAtIndexedSubscript:));
Method newMuTableMethod = class_getInstanceMethod(__NSArrayM, @selector(my_objectMuTableAtIndexedSubscript:));
method_exchangeImplementations(oldMuTableMethod, newMuTableMethod);
});
}
- (id)my_objectAtIndexedSubscript:(NSUInteger)idx {
if (idx < 0 || idx >= self.count) {
@try {
return [self my_objectAtIndexedSubscript:idx];
} @catch (NSException *exception) {
NSLog(@"數組越界了~~~~~%@",exception.reason);
return nil;
}
}else{
return [self my_objectAtIndexedSubscript:idx];
}
}
- (id)my_objectMuTableAtIndexedSubscript:(NSUInteger)idx {
if (idx < 0 || idx >= self.count) {
@try {
return [self my_objectMuTableAtIndexedSubscript:idx];
} @catch (NSException *exception) {
NSLog(@"數組越界了~~~~~%@",exception.reason);
return nil;
}
}else{
return [self my_objectMuTableAtIndexedSubscript:idx];
}
}
還有其他的奔潰,以后更新