[crash詳解與防護(hù)] unrecognized selector crash

前言:

unrecognized selector類型的crash是因?yàn)橐粋€對象調(diào)用了一個不屬于它的方法導(dǎo)致的。要解決這種類型的crash,我們先要了解清楚它產(chǎn)生的具體原因和流程。本文先講了消息傳遞機(jī)制和消息轉(zhuǎn)發(fā)機(jī)制的流程,然后對消息轉(zhuǎn)發(fā)流程的一些函數(shù)的使用進(jìn)行舉例,最后指出了對“unrecognized selector類型的crash”的防護(hù)措施。

一、消息傳遞機(jī)制和消息轉(zhuǎn)發(fā)機(jī)制

1. ?消息傳遞機(jī)制(動態(tài)消息派發(fā)系統(tǒng)的工作過程)

當(dāng)編譯器收到[someObject messageName:parameter]消息后,編譯器會將此消息轉(zhuǎn)換為調(diào)用標(biāo)準(zhǔn)的C語言函數(shù)objc_msgSend,如下所示:

objc_msgSend(someObject,@selector(messageName:),parameter)

?該方法會去someObject所屬的類中搜尋其“方法列表”,如果能找到與messageName:相符的方法,就跳轉(zhuǎn)到實(shí)現(xiàn)代碼;找不到就沿著繼承體系繼續(xù)向上找;如果最終還是找不到,就執(zhí)行“消息轉(zhuǎn)發(fā)”操作。

2. 消息轉(zhuǎn)發(fā)機(jī)制

消息轉(zhuǎn)發(fā)分兩大階段:

(1)動態(tài)方法解析:即征詢selector所屬的類的下列方法,看其是否能動態(tài)添加這個未知的選擇子:

//? 缺失的selector是實(shí)例方法調(diào)用+(BOOL)resolveInstanceMethod:(SEL)selector

//? 缺失的selector是類方法調(diào)用+(BOOL)resolveClassMethod:(SEL)selector

該方法的參數(shù)就是那個未知的選擇子,其返回值Boolean類型,表示這個類是否能新增一個實(shí)例方法用以處理此選擇子。(@dynamic屬性沒有實(shí)現(xiàn)setter方法和getter方法,可以在“消息轉(zhuǎn)發(fā)”過程對其實(shí)現(xiàn))

(2)消息轉(zhuǎn)發(fā)

(2.1)“備援接收者”方案----當(dāng)前接收者第二次處理未知選擇子的機(jī)會:運(yùn)行期系統(tǒng)通過下列方法問當(dāng)前接收者,能不能把這條消息轉(zhuǎn)發(fā)給其它接收者來處理:

-(id)forwardingTargetForSelector:(SEL)selector

該方法的參數(shù)就是那個未知的選擇子,其返回值id類型,表示找到的備援對象,找不到就返回nil。(缺點(diǎn):我們無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息。)

?(2.2) 完整的消息轉(zhuǎn)發(fā)

調(diào)用下列方法轉(zhuǎn)發(fā)消息:

-(void)forwardInvocation:(NSInvocation*)invocation

?NSInvocation把尚未處理的那條消息有關(guān)的全部細(xì)節(jié)都封于其中,包括:選擇子、目標(biāo)及參數(shù)。

(a)上面這個方法可以實(shí)現(xiàn)的很簡單:只需改變調(diào)用目標(biāo),使消息在新目標(biāo)上得以調(diào)用即可(與“備援接收者”方案所實(shí)現(xiàn)的方法等效,很少有人采用)。

(b)比較有用的實(shí)現(xiàn)方式為:在觸發(fā)消息前,先以某種方式改變消息內(nèi)容,比如追加另外一個參數(shù),或是改換選擇子等等。

上面的步驟都不能解決問題的話,就會調(diào)用NSObject的doesNotRecognizeSelector拋出異常。

總結(jié):

  消息轉(zhuǎn)發(fā)的全流程,如下圖所示:

“消息轉(zhuǎn)發(fā)”全流程圖

二、舉例

1. 動態(tài)方法解析,即resolveInstanceMethod的使用:

 ?。ㄒ詣討B(tài)方法解析來實(shí)現(xiàn)@dynamic屬性)

//EOCAutoDictionary.h

@interface EOCAutoDictionary : NSObject

@property(nonatomic, strong) NSDate *date;

@end

//EOCAutoDictionary.m

#import "EOCAutoDictionary.h"

@interface EOCAutoDictionary()

@property(nonatomic, strong) NSMutableDictionary *backingStore;

@end

@implementation EOCAutoDictionary

@dynamic date;

- (id)init {

? ? if(self = [super init]) {_backingStore = [NSMutableDictionary new];}

? ? return self;

}

+ (BOOL) resolveInstanceMethod:(SEL)selector {

? ? //selector = "setDate:" 或 "date",_cmd = (SEL)"resolveInstanceMethod:"

? ? NSString *selectorString = NSStringFromSelector(selector);

? ? if([selectorString hasPrefix:@"set"]) {

? ? ? ? // 向類中動態(tài)的添加方法,第三個參數(shù)為函數(shù)指針,指向待添加的方法。最后一個參數(shù)表示待添加方法的“類型編碼”

? ? ? ? class_addMethod(self, selector,(IMP)autoDictionarySetter,"v@:@");

? ? } else {

? ? ? ? class_addMethod(self, selector,(IMP)autoDictionaryGetter,"v@:@");

? ? }

? ? return YES;

}

id autoDictionaryGetter(id self, SEL _cmd) {

? ? // 此時_cmd = (SEL)"date"

? ? // Get the backing store from the object

? ? EOCAutoDictionary *typeSelf = (EOCAutoDictionary *) self;

? ? NSMutableDictionary *backingStore = typeSelf.backingStore;

? ? //the key is simply the selector name

? ? NSString *key = NSStringFromSelector(_cmd);

? ? //Return the value

? ? return [backingStore objectForKey:key];

}

void autoDictionarySetter(id self, SEL _cmd, id value) {

? ? // 此時_cmd = (SEL)"setDate:"

? ? // Get the backing store from the object

? ? EOCAutoDictionary *typeSelf = (EOCAutoDictionary *) self;

? ? NSMutableDictionary *backingStore = typeSelf.backingStore;

? ? /** The selector will be for example, "setDate:".

? ? * We need to remove the "set",":" and lowercase the first letter of the remainder.*/

? ? NSString *selectorString = NSStringFromSelector(_cmd);

? ? NSMutableString *key = [selectorString mutableCopy];

? ? // Remove the ':' at the end

? ? [key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];

? ? // Remove the 'set' prefix

? ? [key deleteCharactersInRange:NSMakeRange(0, 3)];

? ? // Lowercase the first character

? ? NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];

? ? [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];

? ? if(value) {

? ? ? ? [backingStore setObject:value forKey:key];

? ? } else {

? ? ? ? [backingStore removeObjectForKey:key];

? ? }

}

@end

使用date屬性的setter和getter代碼如下:

EOCAutoDictionary *dict = [EOCAutoDictionarynew];

dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];

NSLog(@"dict.date = %@", dict.date);

?2.?forwardingTargetForSelector的使用

注意:上面的resolveInstanceMethod返回YES的話,就無法調(diào)用forwardingTargetForSelector了。

下面的方法,對SLVForwardTarget的對象調(diào)用uppercaseString方法時,轉(zhuǎn)發(fā)給另一個對象"hello WorLD!"來執(zhí)行uppercaseString方法。

@implementation SLVForwardTarget

#pragma mark forwardingTargetForSelector

-(id) forwardingTargetForSelector:(SEL)aSelector {

? ? if(aSelector == @selector(uppercaseString)){

? ? ? ? return @"hello WorLD!";

? ? }

? ? return nil;

}

@end

3.?forwardInvocation的使用

?改變調(diào)用目標(biāo),使消息在新目標(biāo)上得以調(diào)用的例子:

// SLVForwardInvocation.h

@interface SLVForwardInvocation : NSObject

- (id)initWithTarget1:(id)t1 target2:(id)t2;

@end

// SLVForwardInvocation.m

@interface SLVForwardInvocation()

@property(nonatomic, strong)id realObject1;

@property(nonatomic, strong)id realObject2;

@end

@implementation SLVForwardInvocation

- (id)initWithTarget1:(id)t1 target2:(id)t2 {

? ? _realObject1 = t1;

? ? _realObject2 = t2;

? ? return self;

}

系統(tǒng)check實(shí)例是否能response消息呢?如果實(shí)例本身就有相應(yīng)的response,那么就會響應(yīng)之,如果沒有系統(tǒng)就會發(fā)出methodSignatureForSelector消息,尋問它這個消息是否有效?有效就返回對應(yīng)的方法簽名,無效則返回nil。消息轉(zhuǎn)發(fā)機(jī)制使用從這個方法中獲取的信息來創(chuàng)建NSInvocation對象。因此我們必須重寫這個方法,為給定的selector提供一個合適的方法簽名。// Here, we ask the two real objects, realObject1 first, for their method signatures, since we'll be forwarding the message to one or the other of them in -forwardInvocation:.? If realObject1 returns a non-nil method signature, we use that, so in effect it has priority.

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

? ? NSMethodSignature *sig;

? ? sig = [self.realObject1 methodSignatureForSelector:aSelector];

? ? if (sig){

? ? ? ? return sig;

? ? }

? ? sig = [self.realObject2 methodSignatureForSelector:aSelector];

? ? if (sig){

? ? ? ? return sig;

? ? }

? ? return nil;

}

// Invoke the invocation on whichever real object had a signature for it.

- (void)forwardInvocation:(NSInvocation *)invocation {

? ? id target = [self.realObject1 methodSignatureForSelector:[invocation selector]] ? self.realObject1 : self.realObject2;

? ? [invocation invokeWithTarget:target];

  //或者用下列方法

  /* ?id target;

? ?   if([self.realObject1 respondsToSelector:[invocation selector]]) {

? ? ? ?   target = self.realObject1;

? ?   } else if([self.realObject2 respondsToSelector:[invocation selector]]) {

? ? ?    target = self.realObject2;

?    }

? ?   [invocation invokeWithTarget:target]; ? ?*/

}

測試示例:

NSMutableString *string= [NSMutableString new];

NSMutableArray *array = [NSMutableArray new];

id ?proxy = [[SLVForwardInvocation alloc] initWithTarget1:string target2:array];

// Note that we can't use appendFormat:, because vararg methods cannot be forwarded!

[proxy appendString:@"This "];

[proxy appendString:@"is "];

[proxy addObject:string];

[proxy appendString:@"a "];

[proxy appendString:@"test!"];

if([[proxy objectAtIndex:0] isEqualToString:@"This is a test!"]) {

? ? NSLog(@"Appending successful.");?

} else {?

? ? NSLog(@"Appending failed, got: '%@'", proxy);?

}

此處選擇子"appendString:"改變目標(biāo)為mutableString類型,

"addObject:"和"objectAtIndex:"改變目標(biāo)為mutableArray類型。

三、unrecognized selector crash防護(hù)方案

  根據(jù)上面的講解和舉例,我們知道,當(dāng)一個函數(shù)找不到時,runtime提供了三種方式去補(bǔ)救:

(1)調(diào)用resolveInstanceMethod給個機(jī)會讓類添加實(shí)現(xiàn)這個函數(shù);

(2)調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個函數(shù);

(3)調(diào)用forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標(biāo)函數(shù)以其它形式執(zhí)行。

第一種方案:

  對于“unrecognized selector crash”,我們就可以利用消息轉(zhuǎn)發(fā)機(jī)制來進(jìn)行補(bǔ)救。對于使用上面三步中的哪一步來改造比較合適,我們選擇第二步forwardingTargetForSelector。初步分析原因如下:上面的三步接收者均有機(jī)會處理消息。步驟越往后,處理消息的代價就越大。forwardInvocation要通過NSInvocation來執(zhí)行函數(shù),得創(chuàng)建和處理完整的NSInvocation,開銷比較大。但resolveInstanceMethod給類添加不存在的方法,有可能這個方法并不需要,比較多余。用forwardingTargetForSelector將消息轉(zhuǎn)發(fā)給一個對象,開銷較小。

防護(hù)方案如下:

NSObject的類別NSObject+Forwarding來重寫forwardingTargetForSelector方法,讓執(zhí)行的目標(biāo)轉(zhuǎn)移到SLVUnrecognizedSelectorSolveObject里,然后SLVUnrecognizedSelectorSolveObject添加新的方法對未知選擇子進(jìn)行處理。在處理的這一塊兒,可以加上日志.

缺點(diǎn):

(1)類里的forwardingTargetForSelector如果提前返回nil了,就沒辦法執(zhí)行SLVStubProxy里的autoAddMethod方法。另外,未知選擇子對應(yīng)的類里面如果有forwardInvocation方法的話,會優(yōu)先執(zhí)行SLVStubProxy里的autoAddMethod方法,而不會執(zhí)行選擇子對應(yīng)的類里面的forwardInvocation方法。 整個處理流程,完全是按照以上三種方式的前后順序執(zhí)行,一旦一個方式解決了這個函數(shù)調(diào)用的問題,其它方法就不會執(zhí)行。這里得注意工程代碼里,可能就是需要自己的類里處理未知選擇子的情況。

?(2)還有一些selector如:"getServerAnswerForQuestion:reply:"、

"startArbitrationWithExpectedState:hostingPIDs:withSuppression:onConnected:"、

"_setTextColor:"、"setPresentationContextPrefersCancelActionShown:" ?也會攔截到。本來這些selector系統(tǒng)會自己處理的,相當(dāng)于這塊兒的攔截超前了,照這個比較大的缺陷來說,我們還是在第三步forwardInvocation來處理未知選擇子比較好,所以有了下面這個方案。

第二種方案:

消息轉(zhuǎn)發(fā)機(jī)制里的三個步驟處理未知選擇子,步驟越往后,處理消息的代價就越大。但是步驟越往前,我們越有可能攔截到系統(tǒng)的本來能處理的方法,這種方案是以犧牲效率來改善攔截的準(zhǔn)確性的。

防護(hù)方案如下:

NSObject的類別NSObject+Forwarding來重寫forwardInvocation方法,考慮到諸如"_navigationControllerContentInsetAdjustment"的選擇子有可能系統(tǒng)會在自己的forwardInvocation方法里進(jìn)行處理,所以此處先判斷系統(tǒng)的方法能否處理,系統(tǒng)的方法不能處理未知選擇子,再讓執(zhí)行的目標(biāo)轉(zhuǎn)移到未知選擇子處理對象SLVUnrecognizedSelectorSolveObject?里。然后SLVUnrecognizedSelectorSolveObject添加新的方法對未知選擇子進(jìn)行處理。在處理的這一塊兒,可以加上日志信息。

? ? ?以上兩種方案的代碼如下,其中用枚舉SLVUnrecognizedSelectorSolveScheme分別表示上面的兩種方案,可自行修改,這里推薦第二種方案:

// NSObject+Forwarding.m

#import "NSObject+Forwarding.h"

#import "SLVUnrecognizedSelectorSolveObject.h"

typedef NS_ENUM(NSInteger, SLVUnrecognizedSelectorSolveScheme) {?

?SLVUnrecognizedSelectorSolveScheme1, //第一種方案?

?SLVUnrecognizedSelectorSolveScheme2 //第二種方案 ? ? };

@implementation NSObject (Forwarding)

+ (void)load{?

?static dispatch_once_t onceToken;?

?dispatch_once(&onceToken, ^{?

? ? ?SLVUnrecognizedSelectorSolveScheme scheme = SLVUnrecognizedSelectorSolveScheme2;?

?????if(scheme == SLVUnrecognizedSelectorSolveScheme1){?

? ? ?? ? ?? ? ??[[self class] swizzedMethod:@selector(forwardingTargetForSelector:) withMethod:@selector(newForwardingTargetForSelector:)];?

?? ? ?}else if(scheme == SLVUnrecognizedSelectorSolveScheme2){?

? ? ?? ? ?? ? ??[[self class] swizzedMethod:@selector(methodSignatureForSelector:) withMethod:@selector(newMethodSignatureForSelector:)];?

?? ? ?? ? ?? ? ?[[self class] swizzedMethod:@selector(forwardInvocation:) withMethod:@selector(newForwardInvocation:)]; }?

?? ? ?});

}

+(void)swizzedMethod:(SEL)originalSelector withMethod:(SEL )swizzledSelector {?

?Class class = [self class]; Method originalMethod = class_getInstanceMethod(class, originalSelector);?

?Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);?

?BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));?

?if (didAddMethod) {?

?? ? ?? ? ?class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));

?}else{?

?? ? ?? ? ?method_exchangeImplementations(originalMethod, swizzledMethod);?

?} ? }

#pragma mark forwardTarget

-(id) newForwardingTargetForSelector:(SEL)aSelector {?

?SLVUnrecognizedSelectorSolveObject *obj = [SLVUnrecognizedSelectorSolveObject sharedInstance];?

?return obj; ? ?}

- (NSMethodSignature *)newMethodSignatureForSelector:(SEL)sel{?

?SLVUnrecognizedSelectorSolveObject *unrecognizedSelectorSolveObject = [SLVUnrecognizedSelectorSolveObject sharedInstance];?

?return [self newMethodSignatureForSelector:sel]?:[unrecognizedSelectorSolveObject newMethodSignatureForSelector:sel]; ? ?}

- (void)newForwardInvocation:(NSInvocation *)anInvocation{  ?

? ? ?if([self newMethodSignatureForSelector:anInvocation.selector]){?

?? ? ?? ? ?[self newForwardInvocation:anInvocation];?

?? ? ?? ? ?return;?

? ? ??}?

?? ? ?SLVUnrecognizedSelectorSolveObject *unrecognizedSelectorSolveObject = [SLVUnrecognizedSelectorSolveObject sharedInstance];?

?? ? ?if([self methodSignatureForSelector:anInvocation.selector]){?

?? ? ?? ? ?[anInvocation invokeWithTarget:unrecognizedSelectorSolveObject];?

? ? ??}

}

// SLVUnrecognizedSelectorSolveObject.m

#import "SLVUnrecognizedSelectorSolveObject.h"

@implementation SLVUnrecognizedSelectorSolveObject

+ (instancetype) sharedInstance{

? ? static SLVUnrecognizedSelectorSolveObject *unrecognizedSelectorSolveObject;

? ? static dispatch_once_t? once_token;

? ? dispatch_once(&once_token, ^{

? ?unrecognizedSelectorSolveObject = [[SLVUnrecognizedSelectorSolveObject alloc] init]; });

? ? return unrecognizedSelectorSolveObject;

}

+ (BOOL) resolveInstanceMethod:(SEL)selector {

? ? // 向類中動態(tài)的添加方法,第三個參數(shù)為函數(shù)指針,指向待添加的方法。最后一個參數(shù)表示待添加方法的“類型編碼”

? ? class_addMethod([self class], selector,(IMP)autoAddMethod,"v@:@");

? ? return YES;

}

id autoAddMethod(id self, SEL _cmd) {

? ? //可以在此加入日志信息,棧信息的獲取等,方便后面分析和改進(jìn)原來的代碼。

NSLog(@"unrecognized selector: %@",NSStringFromSelector(_cmd));??

?return 0;

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容