iOS-Runtime之unrecognized selector sent to instance/class 防护Crash

Posted MinggeQingchun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS-Runtime之unrecognized selector sent to instance/class 防护Crash相关的知识,希望对你有一定的参考价值。

可在GitHub上下载示例源代码Demo ,欢迎点赞给星,谢谢!

本文也可参考文章iOS-Runtime消息发送、转发机制

一、报错

ios开发中我们经常会遇到这样的crash

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

二、报错原因

报错原因就是我们调用了一个不存在的方法。

用OC的消息机制来说就是:消息的接收者找不到对应的selector,这样就启动了消息转发机制,我们可以通过代码在消息转发的过程中告诉对象应该如何处理未知的消息,防止程序crash。

默认实现是抛出下面的异常,这样也就crash了

三、解决方案 

Runtime之Method Swizzling拦截

1、Runtime消息机制

Objective-C语言中,方法调用都是类似[receiver selector];的形式,其本质就是让对象在运行时发送消息的过程。
我们来看看方法调用[receiver selector];『编译阶段』『运行阶段』工作

1、编译阶段:[receiver selector];方法被编译器转换为:
 (1)objc_msgSend(receiver,selector) (不带参数)
 (2)objc_msgSend(recevier,selector,org1,org2,…)(带参数)

OC代码示例:

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

通过clang命令查看OC代码编译后代码: 

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、运行时阶段:消息接受者recevier寻找对应的selector。
(1)通过recevier的isa指针找到recevier的Class(类);
(2)在Class(类)的cache(方法缓存)的散列表中寻找对应的IMP(方法实现);
(3)如果在cache(方法缓存)中没有找到对应的IMP(方法实现)的话,就继续在Class(类)的method list(方法列表)中找对应的selector,如果找到,填充到cache(方法缓存)中,并返回selector;
(4)如果在Class(类)中没有找到这个selector,就继续在它的 superClass(父类)中寻找,以此类推,一直查找到根类;
(5)一旦找到对应的selector,直接执行recevier对应selector方法实现的 IMP(方法实现)。
(6)若找不到对应的selector,转向拦截调用,走消息转发;如果没有重写拦截调用的方法,程序就会崩溃。

2、防护原理

   利用Objective-C语言的动态特性,采用AOP(Aspect Oriented Programming)面向切面编程的设计思想,在不侵入原有项目代码的基础之上,通过在 APP 运行时阶段对崩溃因素的的拦截和处理,使得 APP 能够持续稳定正常的运行。
   具象的说,就是对需要Hook的类添加Category(分类),在各个分类中通过Method Swizzling拦截容易造成崩溃的系统方法,将系统原有方法与添加的防护方法的selector(方法选择器)IMP(函数实现指针)进行对调。然后在替换方法中添加防护操作,从而达到避免以及修复崩溃的目的。

3、消息转发、重定向流程

1、消息动态解析:Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。我们可以通过重写这两个方法,添加其他函数实现,并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。若返回 NO 或者没有添加其他函数实现,则进入下一步。
2、消息接受者重定向:如果当前对象实现了 forwardingTargetForSelector:,Runtime 就会调用这个方法,允许我们将消息的接受者转发给其他对象。如果这一步方法返回 nil,则进入下一步。
3、消息重定向:Runtime 系统利用 methodSignatureForSelector: 方法获取函数的参数和返回值类型。
如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。

流程图如下:

从上述三步中,选择哪一步作为入手点最合适,使我们需要考虑的事情
1、resolveInstanceMethod需要在类的本身动态的添加它本身不存在的方法,这些方法对于该类本身来说是冗余的
2、forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写。

3、forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销比较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用来做消息的转发选择机制,不适合多次重写。

我们选择第二步(消息接受者重定向)来进行拦截。因为 -forwardingTargetForSelector 方法可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写。

具体步骤如下:

1、给 NSObject 添加一个分类,在分类中实现一个自定义的 -zm_forwardingTargetForSelector: 方法;
2、利用 Method Swizzling 将 -forwardingTargetForSelector: 和 -zm_forwardingTargetForSelector: 进行方法交换。
3、在自定义的方法中,先判断当前对象是否已经实现了消息接受者重定向和消息重定向。如果都没有实现,就动态创建一个目标类,给目标类动态添加一个方法。
4、把消息转发给动态生成类的实例对象,由目标类动态创建的方法实现,这样 APP 就不会崩溃了。

Method Swizzling 方法的封装

//--------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分类方法实现方法交换

#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

这样APP就不会因为找不到方法而crash了

参考文章

iOS 开发:『Crash 防护系统』(一)Unrecognized Selector

如何建立iOS crash防护机制

以上是关于iOS-Runtime之unrecognized selector sent to instance/class 防护Crash的主要内容,如果未能解决你的问题,请参考以下文章

iOS-Runtime之class_addMethod给类动态添加方法

iOS-Runtime在开发中的使用及相关面试题

预防 app crash 之 unrecognized selector

iOS-Runtime的那些事...编辑中....

ios-Runtime机制

IOS-Runtime(消息机制)