iOS-Runtime's unrecognized selector sent to instance/class protection Crash

You can download the sample source code Demo on GitHub  , welcome to like and star, thank you!

This article can also refer to the article iOS-Runtime message sending and forwarding mechanism

1. Error reporting

We often encounter such crashes in iOS development

unrecognized selector sent to instance 0x******

2. The reason for the error

The reason for the error is that we called a method that does not exist.

In terms of OC's message mechanism: the receiver of the message cannot find the corresponding selector, so the message forwarding mechanism is started. We can tell the object how to handle the unknown message through the code during the message forwarding process to prevent the program from crashing. .

The default implementation is to throw the following exception, which will crash

3. Solutions 

Runtime Method Swizzlinginterception

1. Runtime message mechanism

Objective-CIn languages, method calls are in a similar [receiver selector];form, and its essence is the process of letting an object send a message at runtime.
Let's see how the method call works [receiver selector];in 『编译阶段』and『运行阶段』

1. Compilation phase : [receiver selector]; the method is converted by the compiler to:
 (1) objc_msgSend(receiver, selector) (without parameters)
 (2) objc_msgSend(recevier, selector, org1, org2,…) (with parameters)

OC code example:

NSObject *objc = [[NSObject alloc]init];

clangView the compiled code of OC code  by command:

NSObject *objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

2. Runtime stage: The message receiver recevier looks for the corresponding selector.
(1) Find the Class (class) of the recevier through the isa pointer of the recevier;
(2) Find the corresponding IMP (method implementation) in the hash table of the cache (method cache) of the Class (class); (3)
If in the cache ( If the corresponding IMP (method implementation) is not found in the method cache), continue to find the corresponding selector in the method list (method list) of the Class (class), if found, fill it into the cache (method cache) and return the selector ;
(4) If the selector is not found in the Class (class), continue to search in its superClass (parent class), and so on, until the root class is found; (5) Once the corresponding selector is found, execute it
directly The recevier corresponds to the IMP (method implementation) implemented by the selector method.
(6) If the corresponding selector cannot be found, turn to the interception call and forward the message; if there is no method to rewrite the interception call, the program will crash.

2. Protection principle

   Utilizing Objective-Cthe dynamic characteristics of the language, adopting AOP(Aspect Oriented Programming)the design idea of ​​aspect-oriented programming, without invading the original project code, by intercepting and processing the crash factors during the APP runtime stage, the APP can continue to run stably and normally.
   Concretely speaking, it is to add classes that require Hooks Category(分类), intercept system methods that are likely to cause crashes in each category , and swap the original methods of the system with Method Swizzlingthe added protection methods . Then add guard actions in the replacement method to avoid and fix crashes.selector(方法选择器)IMP(函数实现指针)

3. Message forwarding and redirection process

1. Message dynamic analysis : Objective-C runtime will call +resolveInstanceMethod: or +resolveClassMethod:, giving you the opportunity to provide a function implementation. We can rewrite these two methods, add other functions to implement, and return YES, then the runtime system will restart the process of sending a message. If it returns NO or no other function implementation is added, go to the next step.
2. Message receiver redirection : If the current object implements forwardingTargetForSelector:, Runtime will call this method, allowing us to forward the receiver of the message to other objects. If this step method returns nil, go to the next step.
3. Message redirection : The Runtime system uses the methodSignatureForSelector: method to obtain the parameters and return value types of the function.
If methodSignatureForSelector: returns an NSMethodSignature object (function signature), the Runtime system will create an NSInvocation object and notify the current object through the forwardInvocation: message, giving this message the last chance to find the IMP.
If methodSignatureForSelector: returns nil. Then the Runtime system will issue a doesNotRecognizeSelector: message, and the program will crash.

The flow chart is as follows:

From the above three steps, which step is the most suitable to start with, so that we need to consider things
1. resolveInstanceMethod needs to dynamically add methods that do not exist in the class itself, and these methods are redundant to the class itself 2.
ForwardingTargetForSelector can forward a message to an object, with less overhead and a lower probability of being rewritten, so it is suitable for rewriting.

3. ForwardInvocation can forward messages to multiple objects in the form of NSInvocation, but its overhead is relatively high, and a new NSInvocation object needs to be created, and the function of forwardInvocation is often called by the user as a message forwarding selection mechanism, which is not suitable for multiple times rewrite.

We choose the second step (message receiver redirection) to intercept. Because  -forwardingTargetForSelector the method can forward the message to an object, the overhead is small, and the probability of being rewritten is low, so it is suitable for rewriting.

Specific steps are as follows:

1. Add a category to NSObject, and implement a custom -zm_forwardingTargetForSelector: method in the category;
2. Use Method Swizzling to exchange methods between -forwardingTargetForSelector: and -zm_forwardingTargetForSelector:.
3. In the custom method, first determine whether the current object has implemented message receiver redirection and message redirection. If none of them are implemented, dynamically create a target class and dynamically add a method to the target class.
4. Forward the message to the instance object of the dynamically generated class, which is realized by the method dynamically created by the target class, so that the APP will not crash.

Encapsulation of the Method Swizzling method

//--------NSObject+MethodSwizzling.h代码
@interface NSObject (MethodSwizzling)

// 判断是否是系统类
static inline BOOL isSystemClass(Class cls) {
    BOOL isSystem = NO;
    NSString *className = NSStringFromClass(cls);
    if ([className hasPrefix:@"NS"] || [className hasPrefix:@"__NS"] || [className hasPrefix:@"OS_xpc"]) {
        isSystem = YES;
        return isSystem;
    }
    NSBundle *mainBundle = [NSBundle bundleForClass:cls];
    if (mainBundle == [NSBundle mainBundle]) {
        isSystem = NO;
    }else{
        isSystem = YES;
    }
    return isSystem;
}

/** 交换两个类方法的实现
 * @param originalSelector  原始方法的 SEL
 * @param swizzledSelector  交换方法的 SEL
 * @param targetClass  类
 */
+ (void)defenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;

/** 交换两个对象方法的实现
 * @param originalSelector  原始方法的 SEL
 * @param swizzledSelector 交换方法的 SEL
 * @param targetClass  类
 */
+ (void)defenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;

@end




//--------NSObject+MethodSwizzling.代码
#import "NSObject+MethodSwizzling.h"

#import <objc/runtime.h>

@implementation NSObject (MethodSwizzling)

+ (void)defenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
    swizzlingClassMethod(targetClass, originalSelector, swizzledSelector);
}

+ (void)defenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
    swizzlingInstanceMethod(targetClass, originalSelector, swizzledSelector);
}

// 交换两个类方法的实现
void swizzlingClassMethod(Class class, SEL originalSelector, SEL swizzledSelector) {

    Method originalMethod = class_getClassMethod(class, originalSelector);
    Method swizzledMethod = class_getClassMethod(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);
    }
}

// 交换两个对象方法的实现
void swizzlingInstanceMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
    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);
    }
}

@end

NSObject+SelectorForwarding classification method realizes method exchange

#import "NSObject+SelectorForwarding.h"

#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>

@implementation NSObject (SelectorForwarding)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        // 拦截 `+forwardingTargetForSelector:` 方法,替换自定义实现
        [NSObject defenderSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
                                       withMethod:@selector(zm_forwardingTargetForSelector:)
                                        withClass:[NSObject class]];
        
        // 拦截 `-forwardingTargetForSelector:` 方法,替换自定义实现
        [NSObject defenderSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
                                          withMethod:@selector(zm_forwardingTargetForSelector:)
                                           withClass:[NSObject class]];
        
    });
}

// 自定义实现 `+zm_forwardingTargetForSelector:` 方法
+ (id)zm_forwardingTargetForSelector:(SEL)aSelector {
    SEL forwarding_sel = @selector(forwardingTargetForSelector:);
    
    // 获取 NSObject 的消息转发方法
    Method root_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
    // 获取 当前类 的消息转发方法
    Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);
    
    // 判断当前类本身是否实现第二步:消息接受者重定向
    BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
    
    // 如果没有实现第二步:消息接受者重定向
    if (!realize) {
        // 判断有没有实现第三步:消息重定向
        SEL methodSignature_sel = @selector(methodSignatureForSelector:);
        Method root_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel);
        
        Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel);
        realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
        
        // 如果没有实现第三步:消息重定向
        if (!realize) {
            // 创建一个新类
            NSString *errClassName = NSStringFromClass([self class]);
            NSString *errSel = NSStringFromSelector(aSelector);
        
            NSLog(@"*** Crash Message: +[%@ %@]: unrecognized selector sent to class %p ***",errClassName, errSel, self);
            
            
            NSString *className = @"CrachClass";
            Class cls = NSClassFromString(className);
            
            // 如果类不存在 动态创建一个类
            if (!cls) {
                Class superClsss = [NSObject class];
                cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
                // 注册类
                objc_registerClassPair(cls);
            }
            // 如果类没有对应的方法,则动态添加一个
            if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
                class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
            }
            // 把消息转发到当前动态生成类的实例对象上
            return [[cls alloc] init];
        }
    }
    return [self zm_forwardingTargetForSelector:aSelector];
}

// 自定义实现 `-zm_forwardingTargetForSelector:` 方法
- (id)zm_forwardingTargetForSelector:(SEL)aSelector {
    
    SEL forwarding_sel = @selector(forwardingTargetForSelector:);
    
    // 获取 NSObject 的消息转发方法
    Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
    // 获取 当前类 的消息转发方法
    Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
    
    // 判断当前类本身是否实现第二步:消息接受者重定向
    BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
    
    // 如果没有实现第二步:消息接受者重定向
    if (!realize) {
        // 判断有没有实现第三步:消息重定向
        SEL methodSignature_sel = @selector(methodSignatureForSelector:);
        Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);
        
        Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
        realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
        
        // 如果没有实现第三步:消息重定向
        if (!realize) {
            // 创建一个新类
            NSString *errClassName = NSStringFromClass([self class]);
            NSString *errSel = NSStringFromSelector(aSelector);
    
            NSLog(@"*** Crash Message: -[%@ %@]: unrecognized selector sent to instance %p ***",errClassName, errSel, self);
            
            NSString *className = @"CrachClass";
            Class cls = NSClassFromString(className);
            
            // 如果类不存在 动态创建一个类
            if (!cls) {
                Class superClsss = [NSObject class];
                cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
                // 注册类
                objc_registerClassPair(cls);
            }
            // 如果类没有对应的方法,则动态添加一个
            if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
                class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
            }
            // 把消息转发到当前动态生成类的实例对象上
            return [[cls alloc] init];
        }
    }
    return [self zm_forwardingTargetForSelector:aSelector];
}

// 动态添加的方法实现
static int Crash(id slf, SEL selector) {
    return 0;
}

@end

In this way, the APP will not crash because the method cannot be found.

reference article

iOS Development: "Crash Protection System" (1) Unrecognized Selector

How to build an iOS crash protection mechanism

Guess you like

Origin blog.csdn.net/MinggeQingchun/article/details/118060624