首页 > 其他 > 详细

[crash详解与预防] unrecognized selector crash

时间:2017-02-19 18:16:04      阅读:314      评论:0      收藏:0      [点我收藏+]

前言:

  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方法,让执行的目标转移到SLVStubProxy里,然后SLVStubProxy添加新的方法对未知选择子进行处理。在处理的这一块儿,可以加上日志信息。

////  NSObject+Forwarding.m
#import "SLVStubProxy.h"
#import <objc/runtime.h>

@implementation NSObject (Forwarding)
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(forwardingTargetForSelector:);
        SEL swizzledSelector = @selector(newForwardingTargetForSelector:);
        // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        /**
         *  我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
         *  而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
         *  所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
         */
        
        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);
        }
    });
}

-(id) newForwardingTargetForSelector:(SEL)aSelector {
    SLVStubProxy *proxy = [SLVStubProxy new];
    return proxy;
}


// SLVStubProxy.m
#import "SLVStubProxy.h"
#import <objc/runtime.h>

@implementation SLVStubProxy
+ (BOOL) resolveInstanceMethod:(SEL)selector {
    
    // 向类中动态的添加方法,第三个参数为函数指针,指向待添加的方法。最后一个参数表示待添加方法的“类型编码”
    class_addMethod(self, selector,(IMP)autoAddMethod,"v@:@");
   
    return YES;
}

id autoAddMethod(id self, SEL _cmd) {
    return 0;
}
@end

缺点:

(1)类里的forwardingTargetForSelector如果提前返回nil了,就没办法执行SLVStubProxy里的autoAddMethod方法另外,未知选择子对应的类里面如果有forwardInvocation方法的话,会优先执行SLVStubProxy里的autoAddMethod方法,而不会执行选择子对应的类里面的forwardInvocation方法。 整个处理流程,完全是按照以上三种方式的前后顺序执行,一旦一个方式解决了这个函数调用的问题,其它方法就不会执行。这里得注意工程代码里,可能就是需要自己的类里处理未知选择子的情况。

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

"startArbitrationWithExpectedState:hostingPIDs:withSuppression:onConnected:"、

"_setTextColor:"、"setPresentationContextPrefersCancelActionShown:"  也会拦截到,后面分析一下流程,如果此处不拦截,这些都是在哪里处理的。

[crash详解与预防] unrecognized selector crash

原文:http://www.cnblogs.com/Xylophone/p/6394042.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!