標籤:app rms 機制 cto 另一個 protect sig lan unsigned
處理unrecognized selector異常原因
假如封裝一個方法,在其他模組調用該方法時,傳入參數不匹配則crash
。比如下面的方法:本應該傳入的參數類型為NSMutableArray
,如果傳入的參數類型是NSArray
,導致拋出 unrecognized selector
異常
123 |
- (void)doSomethingWithArray:(NSMutableArray *)arr{[arr addObject:@"123"];} |
當然,通過 參數類型判斷 也可以避免問題的發生:
1234567 |
- (void)doSomethingWithArray:(NSMutableArray *)arr{if ([arr isKindOfClass:[NSMutableArray class]]) {[arr addObject:@"123"];}else{CrashOnSimulator(@"??參數類型不對哦??");}} |
crash提醒:
123 |
void CrashOnSimulator(NSString *errorMsg) {if((TARGET_OS_SIMULATOR)){raise(SIGSTOP);}} |
但是,有點地方可能忘記類型判斷了怎麼辦,有全域攔截unrecognized selector
異常的方案嗎?
分析 如何全域攔截unrecognized selector 異常
oc
的訊息發送機制咱們都熟悉了,通過superclass
指標逐級向上尋找該訊息所對應的方法實現,如果遇到找不的方法,還有三次補救機制。我們可以通過上面三種方法中的一種,就可以避免unrecognized selector sent to instance
第一種方法:重寫 NSObject 的forwardingTargetForSelector:
??filter unrecoginze seletor of intance only
思路
- 建立一個接收未知訊息的類,暫且稱之為
Protector
- 建立一個
NSObject
的分類,在分類中重寫forwardingTargetForSelector
: ,在這個方法中截獲未實現的方法,轉寄給Protector
。並為Protector
動態添加未實現的方法,最後返回Protector
的執行個體對象。
- 在分類中新增一個安全的方法實現,來作為
Protector
接收到的未知訊息的實現
代碼
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374 |
#import "NSObject+Protector.h"#import <objc/runtime.h>@implementation NSObject (Protector) #pragma clang diagnostic push#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"- (id)forwardingTargetForSelector:(SEL)aSelector{ if ([self isCurrentClassInWhiteList]) {[[self class] warningDeveloper:aSelector]; Class protectorCls = NSClassFromString(@"ProtectorClassName");if (!protectorCls){protectorCls = objc_allocateClassPair([NSObject class], "ProtectorClassName", 0);objc_registerClassPair(protectorCls);} if (![self isExistSelector:aSelector inClass:protectorCls]){class_addMethod(protectorCls, aSelector, [self safeImplementation:aSelector],[NSStringFromSelector(aSelector) UTF8String]);} Class Protector = [protectorCls class];id instance = [[Protector alloc] init];return instance;} else {return nil;}}#pragma clang diagnostic pop - (BOOL)isCurrentClassInWhiteList{NSArray *classNameArray = @[@"NSNull",@"NSString",@"NSArray",@"NSDictionary",@"NSURL"];for (NSString *className in classNameArray) {if ([self isKindOfClass:NSClassFromString(className)]) {return YES;}}return NO;} - (BOOL)isExistSelector: (SEL)aSelector inClass:(Class)currentClass{BOOL isExist = NO;unsigned int methodCount = 0;Method *methods = class_copyMethodList(currentClass, &methodCount);for (int i = 0; i < methodCount; i++){Method temp = methods[i];SEL sel = method_getName(temp);NSString *methodName = NSStringFromSelector(sel);if ([methodName isEqualToString: NSStringFromSelector(aSelector)]){isExist = YES;break;}}return isExist;} - (IMP)safeImplementation:(SEL)aSelector{IMP imp = imp_implementationWithBlock(^(){NSLog(@"PROTECTOR: %@ Done", NSStringFromSelector(aSelector));});return imp;} + (void)warningDeveloper:(SEL)aSelector{#if DEBUGNSString *selectorStr = NSStringFromSelector(aSelector);NSLog(@"PROTECTOR: -[%@ %@]", [self class], selectorStr);NSLog(@"PROTECTOR: unrecognized selector \"%@\" sent to instance: %p", selectorStr, self);NSLog(@"PROTECTOR: call stack: %@", [NSThread callStackSymbols]);// @throw @"方法找不到";#endif} @end |
第二種方法:重寫 NSObject 的methodSignatureForSelector(有些問題,下面有個最終版)
??filter unrecoginze seletor of class and intance
but 如果你使用了JSPatch、Aspects等對methodSignatureForSelector進行swizzle的第三方庫,就別用這種方案了,有衝突,出現莫名的錯誤
思路
- 建立
NSObject+Protector
重寫methodSignatureForSelector
,判斷current class
是否在白名單?
YES
:返回一個空簽名,啥也不做
NO
:返回正常的簽名,走原來的邏輯
代碼
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 |
#import "NSObject+Protector.h"#import <objc/runtime.h>@implementation NSObject (Protector) #pragma clang diagnostic push#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ return [[self class] __getMethodSignatureForSelector:aSelector];} - (void)forwardInvocation:(NSInvocation *)anInvocation{}#pragma clang diagnostic pop + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ return [self __getMethodSignatureForSelector:aSelector];} + (void)forwardInvocation:(NSInvocation *)anInvocation{} + (NSMethodSignature *)__getMethodSignatureForSelector:(SEL)aSelector{ if ([self isSubclassInWhiteListClass]) { [self warningDeveloper:aSelector]; return [NSMethodSignature signatureWithObjCTypes:"@"]; }else{ return [self instanceMethodSignatureForSelector:aSelector]; }} + (BOOL)isSubclassInWhiteListClass{ NSArray *classNameArray = @[@"NSNull",@"NSString",@"NSArray",@"NSDictionary",@"NSURL"]; for (NSString *className in classNameArray) { if ([self isSubclassOfClass:NSClassFromString(className)]) { return YES; } } return NO;} + (void)warningDeveloper:(SEL)aSelector{#if DEBUG NSString *selectorStr = NSStringFromSelector(aSelector); NSLog(@"PROTECTOR: -[%@ %@]", [self class], selectorStr); NSLog(@"PROTECTOR: unrecognized selector \"%@\" sent to instance: %p", selectorStr, self); NSLog(@"PROTECTOR: call stack: %@", [NSThread callStackSymbols]); // @throw @"方法找不到";#endif} |
第三種方法:最終方案(解決methodSignatureForSelector的不足)
第三方庫對 methodSignatureForSelector進行了全域替換,而我們也在NSObject中 進行了全域替換,衝突的點在於我們影響了第三庫的自訂的Class。<br\>
- 如何避免呢? 我們替換常用的的幾個class就行了唄,是的,不過工作量有點大且重複,怎麼辦?用 define 來解決,如下:
1234567891011121314151617181920212223242526272829303132333435 |
// NSArray+WBGProtector.m #import "NSArray+WBGProtector.h"#import <objc/runtime.h> #define WBG_PROTECT_CLASS_NAME(_classname_)\@implementation _classname_ (WBGProtector)\\- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{\return [[self class] __getMethodSignatureForSelector:aSelector type:@"instance"];\}\\- (void)forwardInvocation:(NSInvocation *)anInvocation{\}\\+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{\return [self __getMethodSignatureForSelector:aSelector type:@"class"];\}\\+ (void)forwardInvocation:(NSInvocation *)anInvocation{\}\\+ (NSMethodSignature *)__getMethodSignatureForSelector:(SEL)aSelector type:(id)type{\NSString *errorMsg = [NSString stringWithFormat:@"PROTECTOR: -[%@ %@],unrecognized selector sent to %@",self,NSStringFromSelector(aSelector),type];\NSLog(@"%@",errorMsg);\CrashOnSimulator(errorMsg);\return [NSMethodSignature signatureWithObjCTypes:"@"];\}\\@end\ WBG_PROTECT_CLASS_NAME(NSArray)WBG_PROTECT_CLASS_NAME(NSDictionary)WBG_PROTECT_CLASS_NAME(NSString) |
細節分析為什麼需要白名單?
在app
啟動載入一些系統方法,總是莫名其名的 報錯 甚至crash
為什麼去掉
UIResponder
?
isCurrentClassInWhiteList
中的classNameArray
本來是有 UIResponder
的,但是後來測試發現UIWebView
會出現異常!這裡把UIResponder
去掉了,畢竟過濾大部分的unrecognize selector
主要的是NSArray
和NSDictionary
測試
code
1234567 |
// test Class methodid clazz = [NSArray class];[clazz viewDidLoad]; // test instance methodNSMutableArray *arr = @{};[arr addObject:@""] |
遺留問題
- 需要判斷自己項目中引入的第三方庫沒有通過
category
的方式去重寫NSObject
的方法methodSignatureForSelector /forwardInvocation
以及forwardingTargetForSelector
- 原因:一個category也不能可靠的覆蓋另一個category中相同的類的相同的方法。例如UIViewController+A與UIViewController+B,都重寫了viewDidLoad,我們就無法控制誰覆蓋了誰。
- 如果第三方重寫了,則在這裡通過swizzling的方式 替換 具體的實現方法
預防 app crash 之 unrecognized selector