zoukankan      html  css  js  c++  java
  • [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:以上代码自己写的,有待进一步检验和改进。

  • 相关阅读:
    Java Spring Boot VS .NetCore (十) Java Interceptor vs .NetCore Interceptor
    Java Spring Boot VS .NetCore (九) Spring Security vs .NetCore Security
    IdentityServer4 And AspNetCore.Identity Get AccessToken 问题
    Java Spring Boot VS .NetCore (八) Java 注解 vs .NetCore Attribute
    Java Spring Boot VS .NetCore (七) 配置文件
    Java Spring Boot VS .NetCore (六) UI thymeleaf vs cshtml
    Java Spring Boot VS .NetCore (五)MyBatis vs EFCore
    Java Spring Boot VS .NetCore (四)数据库操作 Spring Data JPA vs EFCore
    Java Spring Boot VS .NetCore (三)Ioc容器处理
    Java Spring Boot VS .NetCore (二)实现一个过滤器Filter
  • 原文地址:https://www.cnblogs.com/Xylophone/p/6394042.html
Copyright © 2011-2022 走看看