阅读这篇文章你将知道OC消息转发的三步处理可以用来做什么

最推荐阅读🌟:

《iOS 开发:『Runtime』详解(一)基础知识》 https://bujige.net/blog/iOS-Runtime-01.html

其他推荐阅读:

《Objective-C Runtime》https://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/

网络上关于Objective-C Runtime总结文章非常多,我这里就取最最精华的部分,只放两篇。其中第一篇是我看过的条理最清晰,最易懂的Runtime文章。

我的学习与思考:

我在阅读介绍Runtime的博客时发现,所有的文章都会讲[dog run];这样一个调用流程(不了解Objective-C语法的可以认为是dog对象调用了run()函数):

首先会进行消息发送,去dog对象所属类的函数表(methodLists)中去找是否实现了run函数,实现了则执行run函数。如果没有实现run函数,Objective-C的Runtime则会进入消息转发流程,进行消息动态解析消息接受者重定向消息重定向三步处理,如果这三步处理都失败了,就会抛出我们熟悉的unrecognized selector sent to instance异常。

但是却很少有文章提到我们能用消息转发的这三处理来做什么。通过问组里的大哥和查阅资料了解到,消息转发的三步流程在业务上使用较少,主要使用在程序的crash防护中,这就解答了我的困惑。下面用一个demo来展示下是如何利用消息转发机制对unrecognized selector sent to instance这种crash进行防护的。

[dog run]中run函数没有实现时,消息转发给我们提供了三步处理来挽回。
1.在消息动态解析中调用+resolveInstanceMethod:为该类动态添加函数的实现
2.在消息接受者重定向中调用-forwardingTargetForSelector:将消息转发给别的对象(仅限一个),看看ta有没有实现run函数
3.在消息重定向中调用-forwardInvocation:将消息转给多个对象,再试一下

Demo实现:

这里我们在第2步进行拦截,为什么是在第2步,网易前端技术团队https://neyoufan.github.io/2017/01/13/ios/BayMax_HTSafetyGuard/ 给了原因

  1. +resolveInstanceMethod: 需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的
  2. -forwardInvocation:可以通过NSInvocation的形式将消息转发给多个对象,但是其开销较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写
  3. -forwardingTargetForSelector:可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写

1.为NSObject添加Hook拦截-forwardingTargetForSelector:

Hook用于拦截系统-forwardingTargetForSelector:的执行,替换为我们重写的- (id)crashProtector_forwardingTargetForSelector:

#import "NSObject+HookJQ.h"

@implementation NSObject (HookJQ)

+ (void)crashProtectorSwizzlingClass:(Class)class
                    originalSelector:(SEL)originalSelector
                    swizzledSelector:(SEL)swizzledSelector{
    [self swizzlingInstanceMethodWithClass:class originalSelector:originalSelector swizzledSelector:swizzledSelector];
}

- (void)swizzlingInstanceMethodWithClass:(Class)class
                        originalSelector:(SEL)originalSelector
                        swizzledSelector:(SEL)swizzledSelector{
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end

2.重写-forwardingTargetForSelector:方法

动态创建名为crashProtectorClass的crashProtector类,并为其动态添加一个方法,文中的方法传入什么都直接返回0,然后把消息转发到这个动态创建的类的实例对象,完成crash防护。

#import "NSObject+CrashProtectorJQ.h"
#import "NSObject+HookJQ.h"
@implementation NSObject (CrashProtectorJQ)

+ (void)load{
    //拦截系统的方法,替换成我们的实现
    [NSObject crashProtectorSwizzlingClass:[NSObject class] originalSelector:@selector(forwardingTargetForSelector:) swizzledSelector:@selector(crashProtector_forwardingTargetForSelector:)];
}

//重写的forwardingTargetForSelector:方法
- (id)crashProtector_forwardingTargetForSelector:(SEL)aSelector{
    //随意动态创建一个类
    Class crashProtectorClass = NSClassFromString(@"crashProtector");
    if (!crashProtectorClass) {
        crashProtectorClass = objc_allocateClassPair([NSObject class], "crashProtector", 0);
        //注册该类
        objc_registerClassPair(crashProtectorClass);
    }
    //如果该类没有实现那个方法,动态添加一个实现
    if (!class_getInstanceMethod(NSClassFromString(@"crashProtector"), aSelector)) {
        class_addMethod(crashProtectorClass, aSelector, (IMP)crashPro, "@@:@");
    }
    //转发到crashProtector的实例对象
    return [crashProtectorClass new];
}
int crashPro(id slf ,SEL selector){
    return 0;
}
@end

3.测试

新建一个类TestObj继承自NSObject,声明thisEmptyMethod方法,但不实现

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

@interface TestObj : NSObject

- (void)thisEmptyMethod;

@end

@implementation TestObj

@end

程序可以正常运行,不会crash

注释掉crash防护方法,发生unrecognized selector sent to instance异常,程序崩溃