[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"

@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 = [EOCAutoDictionarynew];

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

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 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:"改变目标为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"

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 {

    // 向类中动态的添加方法,第三个参数为函数指针,指向待添加的方法。最后一个参数表示待添加方法的“类型编码”

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

    return YES;

}

id autoAddMethod(id self, SEL _cmd) {

    //可以在此加入日志信息,栈信息的获取等,方便后面分析和改进原来的代码。

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

 return 0;

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270