阅读这篇文章你将知道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/ 给了原因
- +resolveInstanceMethod: 需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的
- -forwardInvocation:可以通过NSInvocation的形式将消息转发给多个对象,但是其开销较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写
- -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
异常,程序崩溃