預防 app crash 之 unrecognized selector

來源:互聯網
上載者:User

標籤: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主要的是NSArrayNSDictionary

測試 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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.