iOS 从runtime理解消息转发原理和用途
前言
从一个对象收到一个无法响应的方法到崩溃之间发生了什么?runtime在底层做了哪些操作?
OC对象在发送消息的时候会在该类的缓存列表中查找方法,如果找到直接调用相关方法的实现,如果没有会到类的struct objc_method_list列表中去搜索,如果找到则直接调用相关方法的实现,如果没有找到就会通过super_class指针沿着继承树向上去搜索。如果到了根部还没有找到,这是runtime大神就会帮我们做些事情,防止我们的引用崩溃。
我们在开发中经常会遇到这样的报错,当我们调用一个无响应的方法
self.testVC = [[TestViewController alloc]init];
[self.testVC performSelector:@selector(drink)];
[self.testVC drink];
在控制台会打印出如下错误提示
![](https://img.haomeiwen.com/i11244884/d18dc707dbf683cf.png)
上面两种方法在执行过程中有什么区别呢?
![](https://img.haomeiwen.com/i11244884/a70e08557e4babc2.png)
官方文档说的很清楚,执行的效果其实和发送消息是等价的,但是 performSelector可以发送在运行时没有确定的消息。当我们为类动态添加方法是,必须用此种方法进行调用,为了避免运行时出现错误,在使用performSelector之前一定要使用如下的检查方法来进行判断。
- (BOOL)respondsToSelector:(SEL)aSelector;
消息转发
- 类的动态方法解析。
在该阶段如果调用了类方法会进行+(BOOL)resolveInstanceMethod:(SEL)sel判断,如果调用了类方法 首先会进行 +(BOOL)resolveClassMethod:(SEL)sel判断,如果返回YES,则这个类可以接受消息,如果返回NO,则不能接受消息会进入下一步处理。
//方法的调用
TestViewController *testVC = [[TestViewController alloc]init];
/******类方法调用******/
[TestViewController performSelector:@selector(testClassFunction)];
/******对象方法调用******/
[testVC performSelector:@selector(testFunction)];//此处必须用performSelector这个方法
/**
类:如果是类方法的调用,首先会触发该类方法
@param sel 传递进入的方法
@return 如果YES则能接受消息 NO不能接受消息 进入第二步
*/
+(BOOL)resolveClassMethod:(SEL)sel{
if ([NSStringFromSelector(sel) isEqualToString:@"testClassFunction"]) {
/**
对类进行添加类方法 需要讲方法添加进入元类内
*/
[self addMethodWithClass:object_getClass([self class]) withMethodSel:sel withImpMethodSel:@selector(addClassDynamicMethod)];
return YES;
}
return [super resolveClassMethod:sel];
}
/**
对象:在接受到无法解读的消息的时候 首先会调用所属类的类方法
@param sel 传递进入的方法
@return 如果YES则能接受消息 NO不能接受消息 进入第二步
*/
+(BOOL)resolveInstanceMethod:(SEL)sel{
//判断是否为外部调用的方法
if ([NSStringFromSelector(sel) isEqualToString:@"testFunction"]) {
/**
对类进行对象方法 需要把方法添加进入类内
*/
[self addMethodWithClass:[self class] withMethodSel:sel withImpMethodSel:@selector(addDynamicMethod)];
return YES;
}
return [super resolveInstanceMethod:sel];
}
+(void)addMethodWithClass:(Class)class withMethodSel:(SEL)methodSel withImpMethodSel:(SEL)impMethodSel{
//获取实现的方法内容
Method funtionRealMethod = class_getInstanceMethod(class, impMethodSel);
//实现方法的IMP
IMP methodIMP = method_getImplementation(funtionRealMethod);
//实现方法的类别 返回类型 携带参数 等
const char * types = method_getTypeEncoding(funtionRealMethod);
//对目标添加方法
class_addMethod(class, methodSel, methodIMP, types);
}
-(void)addDynamicMethod{
NSLog(@"动态添加方法");
}
+(void)addClassDynamicMethod{
NSLog(@"动态添加类方法");
}
类方法是存储在元类里面,object_getClass([self class])是获取元类对象。动态给类添加方法主要是class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 这个方法。第一个参数是需要添加方法的类,第二个参数是一个selector,也就是实例方法的名字,第三个参数是一个IMP类型的变量也就是函数实现,需要传入一个C函数,这个函数至少有两个参数,一个是id self一个是SEL _cmd,第四个参数是函数类型。
- 备用接受者
在类的动态方法解析中返回了NO,这事会进入备用接受者阶段,需要重写(id)forwardingTargetForSelector:(SEL)aSelector 方法 并返回一个备用接受者对象 。
创建一个继承NSObject的类WOUnrecognizedSelectorSolveObject,在.m文件中实现动态添加备用接受需要实现的方法,用来当作备用响应者,代码如下。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
class_addMethod([self class], sel, (IMP)addMethod, "v@:@");
return YES;
}
id addMethod(id self, SEL _cmd) {
NSLog(@"WOCrashProtector: unrecognized selector: %@", NSStringFromSelector(_cmd));
return 0;
}
在NSObject的分类中重写下列的方法。
- (id)forwardingTargetForSelector:(SEL)aSelector {
IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));
//此处是判断类是否已实现调用
if (imp != impOfNSObject) {
//NSLog(@"class has implemented invocation");
return nil;
}
//创建备用接受者去处理消息
WOUnrecognizedSelectorSolveObject *solveObject = [WOUnrecognizedSelectorSolveObject new];
solveObject.objc = self;
// [solveObject runTests];
return solveObject;
}
在上面方法返回WOUnrecognizedSelectorSolveObject,并且在WOUnrecognizedSelectorSolveObject中动态添加了可以响应的方法,所以就不会再崩溃了。对于本身类无法响应的方法可以找一个对象来完成。
- 完整的消息转发
如果第2步返回self或者nil,则说明没有可以响应的目标 则进入第三步。
第三步的消息转发机制本质上跟第二步是一样的都是切换接受消息的对象,但是第三步切换响应目标更复杂一些,第二步里面只需返回一个可以响应的对象就可以了,第三步还需要手动将响应方法切换给备用响应对象。
//第三步的消息转发机制本质上跟第二步是一样的都是切换接受消息的对象
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
//如果返回为nil则进行签名
if ([super methodSignatureForSelector:aSelector]==nil) {
BackupTestMessage * backUp = [BackupTestMessage new];
NSMethodSignature * sign = [backUp methodSignatureForSelector:aSelector];
return sign;
}
return [super methodSignatureForSelector:aSelector];
}
//上方方法如果调用返回有签名 则进入消息转发最后一步
//JSPatch 就是使用了该方法 来做了动态热更新
-(void)forwardInvocation:(NSInvocation *)anInvocation{
//创建备用对象
BackupTestMessage * backUp = [BackupTestMessage new];
SEL sel = anInvocation.selector;
//判断备用对象是否可以响应传递进来等待响应的SEL
if ([backUp respondsToSelector:sel]) {
[anInvocation invokeWithTarget:backUp];
}else{
// 如果备用对象不能响应 则抛出异常
[self doesNotRecognizeSelector:sel];
}
}
runtime可以对崩溃做出补救,使用上面三种方法都可以实现,但是我们选着那种合适呢?
- resolveInstanceMethod需要在类的本身动态的添加它本身不存在的方法,这些方法对于该类本身来说是冗余的。
- forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写。
- forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销比较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用来做消息的转发选择机制,不适合多次重写。
实际用途
- 阻止线上应用Unrecognized Selector类型crash。
- iOS动态化更新方案——JSPatch。
主要是使用runtime提供的一些运行时的API。
//根据类名反射出我们想要的类
Class class = NSClassFromString(@"OneViewController");
//根据方法反色出我们想要的方法
SEL selector = NSSelectorFromString(@"viewDidLoad");
//新增方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
//替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
// 在runtime中动态注册类
Class superCls = NSClassFromString(superClassName);
cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
objc_registerClassPair(cls);
JSPath在实现中是通过创建一个class同名对象,当向OC发送alloc消息之后,会将OC环境中创建的对象地址保存到这个这个js同名对象中,js本身并不完成任何对象的初始化。
那对象是怎样调用方法的呢?
在执行JS脚本之前,先把所有方法的名字替换成_c(‘method’)的方式,使用的是OC消息转发机制,给JS对象基类添加__c方法,这样所有对象都可以调用到__c,在根据对象类型进行不同的操作。
JS怎样替换OC方法?
- 替换原有selector的IMP实现为一个空的IMP实现,这样当objc_class接受到消息之后,就会进行消息转发。
- 将替换的JS方法构造一个JPSelector及其IMP实现,添加到当前class中。
- 然后改写每个替换方法类的forwadInvocation的实现进行拦截,如果拦截到的Invocation的selctor转化成JPSelector能够响应,说明是一个替换方法,则从Invocation中取参数后调用JPSelector的IMP。
消息转发实现多继承
@implementation Programmer
// 通过消息转发实现多继承
- (id)forwardingTargetForSelector:(SEL)aSelector {
Singer *singer = [[Singer alloc] init];
Artist *artist = [[Artist alloc] init];
if ([singer respondsToSelector:aSelector]) {
return singer;
}
else if ([artist respondsToSelector:aSelector]) {
return artist;
}
return nil;
}
@end
@interface Artist : NSObject
// 画家可以绘画
- (void)draw;
@end
@implementation Artist
- (void)draw {
NSLog(@"I can draw");
}
@end
@interface Singer : NSObject
// 歌手会唱歌
- (void)sing;
@end
@implementation Singer
- (void)sing {
NSLog(@"Lalalalalala");
}
@end
//调用
Programmer *programmer = [[Programmer alloc] init];
// 或者通过类型强转来实现,无警告
[(Artist *)programmer draw];
[(Singer *)programmer sing];