[crash詳解與防護] unrecognized selector crash


前言:

  unrecognized selector類型的crash是因為一個對象調用了一個不屬於它的方法導致的。要解決這種類型的crash,我們先要了解清楚它產生的具體原因和流程。本文先講了消息傳遞機制和消息轉發機制的流程,然后對消息轉發流程的一些函數的使用進行舉例,最后指出了對“unrecognized selector類型的crash”的防護措施。 

一、消息傳遞機制和消息轉發機制

1.  消息傳遞機制(動態消息派發系統的工作過程)

當編譯器收到[someObject messageName:parameter]消息后,編譯器會將此消息轉換為調用標准的C語言函數objc_msgSend,如下所示:

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

 該方法會去someObject所屬的類中搜尋其“方法列表”,如果能找到與messageName:相符的方法,就跳轉到實現代碼;找不到就沿着繼承體系繼續向上找;如果最終還是找不到,就執行“消息轉發”操作。

2. 消息轉發機制

  消息轉發分兩大階段:

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

//  缺失的selector是實例方法調用
+(BOOL)resolveInstanceMethod:(SEL)selector
//  缺失的selector是類方法調用
+(BOOL)resolveClassMethod:(SEL)selector

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

(2)消息轉發

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

-(id)forwardingTargetForSelector:(SEL)selector

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

 (2.2) 完整的消息轉發

調用下列方法轉發消息:

-(void)forwardInvocation:(NSInvocation*)invocation

 NSInvocation把尚未處理的那條消息有關的全部細節都封於其中,包括:選擇子、目標及參數。

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

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

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

總結:

  消息轉發的全流程,如下圖所示:

“消息轉發”全流程圖

二、舉例

1. 動態方法解析,即resolveInstanceMethod的使用:

  (以動態方法解析來實現@dynamic屬性)

//EOCAutoDictionary.h
@interface EOCAutoDictionary : NSObject
@property(nonatomic, strong) NSDate *date;
@end

//EOCAutoDictionary.m
#import "EOCAutoDictionary.h"
#import <objc/runtime.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"]) {
        // 向類中動態的添加方法,第三個參數為函數指針,指向待添加的方法。最后一個參數表示待添加方法的“類型編碼”
        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 = [EOCAutoDictionary new];
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date = %@", dict.date);

 2. forwardingTargetForSelector的使用

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

下面的方法,對SLVForwardTarget的對象調用uppercaseString方法時,轉發給另一個對象"hello WorLD!"來執行uppercaseString方法。

@implementation SLVForwardTarget
#pragma mark forwardingTargetForSelector
-(id) forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(uppercaseString)){
        return @"hello WorLD!";
    }
    return nil;
}
@end

測試代碼:

SLVForwardTarget *ft = [SLVForwardTarget new];
NSString * s = [ft performSelector:@selector(uppercaseString)];
NSLog(@"%@",s);
//輸出結果為:“HELLO WORLD!”

 3. forwardInvocation的使用

 改變調用目標,使消息在新目標上得以調用的例子:

// 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;
}

//系統check實例是否能response消息呢?如果實例本身就有相應的response,那么就會響應之,如果沒有系統就會發出methodSignatureForSelector消息,尋問它這個消息是否有效?有效就返回對應的方法簽名,無效則返回nil。消息轉發機制使用從這個方法中獲取的信息來創建NSInvocation對象。因此我們必須重寫這個方法,為給定的selector提供一個合適的方法簽名。
// Here, we ask the two real objects, realObject1 first, for their metho
// 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:"改變目標為mutableString類型,"addObject:"和"objectAtIndex:"改變目標為mutableArray類型。

三、unrecognized selector crash防護方案

  根據上面的講解和舉例,我們知道,當一個函數找不到時,runtime提供了三種方式去補救:

(1)調用resolveInstanceMethod給個機會讓類添加實現這個函數;

(2)調用forwardingTargetForSelector讓別的對象去執行這個函數;

(3)調用forwardInvocation(函數執行器)靈活的將目標函數以其它形式執行。

第一種方案:

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

防護方案如下:

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

缺點:

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

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

"startArbitrationWithExpectedState:hostingPIDs:withSuppression:onConnected:"、

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

第二種方案:

消息轉發機制里的三個步驟處理未知選擇子,步驟越往后,處理消息的代價就越大。但是步驟越往前,我們越有可能攔截到系統的本來能處理的方法,這種方案是以犧牲效率來改善攔截的准確性的。

防護方案如下:

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

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

//  NSObject+Forwarding.m
#import "NSObject+Forwarding.h"
#import "SLVUnrecognizedSelectorSolveObject.h"
#import <objc/runtime.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" #import <objc/runtime.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 { // 向類中動態的添加方法,第三個參數為函數指針,指向待添加的方法。最后一個參數表示待添加方法的“類型編碼” class_addMethod([self class], selector,(IMP)autoAddMethod,"v@:@"); return YES; } id autoAddMethod(id self, SEL _cmd) { //可以在此加入日志信息,棧信息的獲取等,方便后面分析和改進原來的代碼。
  
NSLog(@"unrecognized selector: %@",NSStringFromSelector(_cmd));
return 0;
}

PS:以上代碼自己寫的,有待進一步檢驗和改進。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM