iOS-Runtime消息发送、转发机制

351 阅读13分钟

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

一、报错 在iOS开发中我们经常会遇到这样的crash

unrecognized selector sent to instance 0x****** 二、报错原因 报错原因就是我们调用了一个不存在的方法。

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

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

三、解决方案  Runtime消息转发机制

1、Runtime

runtime 是 OC底层的一套C语言的API(引入 <objc/runtime.h> 或<objc/message.h>),<objc/runtime.h> //包含对类、成员变量、属性、方法的操作;

<objc/message.h> //包含消息机制

编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:

clang -rewrite-objc xxx.m 可以看到编译后的xxx.cpp(C++文件)

2、消息发送和转发流程可以概括为:

消息发送(Messaging)是 Runtime 通过 selector 快速查找 IMP 的过程,有了函数指针就可以执行对应的方法实现;

消息转发(Message Forwarding)是在查找 IMP 失败后执行一系列转发流程的慢速通道,如果不作转发处理,则会打日志和抛出异常。

消息的转发分为三步,如下:

1、第一次转发:动态方法解析 Method resolution 对象在收到无法解读的消息,也就是找不到的方法之后,就会调用如下两个方法:

  • (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); //类方法
  • (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); //实例方法 具体写法可参考如下代码;

在runtime中,一般使用者会引入objc/message.h文件,这个文件用于消息分发,但是运行时对类的加载的文件是在objc/runtime.h文件中

Man类继承自Person类,且Man类和Person类中都没有声明和实现实例方法drinkPear和类方法smoke

Man.m文件

#import "Man.h" #import <objc/runtime.h> //包含对类、成员变量、属性、方法的操作 #import "Son.h"

@implementation Man

  • (instancetype)init { self = [super init]; if (self) { // [self performSelector:@selector(sel) withObject:nil]; }

    return self; }

#pragma mark - 第一次转发:方法解析 Method resolution //实例方法IMP方法名 id dynamicInstanceMethodIMP(id self, SEL _cmd) { NSLog(@"第一次转发:方法解析----%s:实例方法",FUNCTION); return @"1"; }

//类方法IMP方法名 id dynamicClassMethodIMP(id self, SEL _cmd) { NSLog(@"第一次转发:方法解析----%s:类方法",FUNCTION); return @"2"; }

/* class_addMethod方法 class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) Class cls:添加新方法的那个类名(实例方法,传入CLass;类方法,传入MetaClss;可以这样理解,OC里的Class里的加号方法,相当于该类的MetalClas的实例方法,类调用类方法,和对象调用实例方法,其实底层实现都是一样的。类也是对象。) SEL name:要添加的方法名 IMP imp:实现这个方法的函数 const char *types:要添加的方法的返回值和参数;如:"v@:@":v:是添加方法无返回值 @表示是id(也就是要添加的类) :表示添加的方法类型 @表示:参数类型 */

/**实例方法 对象:在接受到无法解读的消息的时候 首先会调用所属类的类方法 @param sel 传递进入的方法 @return 如果YES则能接受消息 NO不能接受消息 进入第二步 */

  • (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(drinkPear)) { //实例方法,传入CLass //对类进行对象方法 需要把方法添加进入类内 class_addMethod([self class], sel, (IMP)(dynamicInstanceMethodIMP), "@@:"); return YES; } return [super resolveInstanceMethod:sel]; }

/**类方法 类:如果是类方法的调用,首先会触发该类方法

@param sel 传递进入的方法 @return 如果YES则能接受消息 NO不能接受消息 进入第二步 */

  • (BOOL)resolveClassMethod:(SEL)sel { if (sel == @selector(smoke)) { //对类进行添加类方法 需要将方法添加进入元类内;将[self class]或self.class替换为object_getClass/objc_getMetaClass //C语言函数写法:(IMP)(dynamicClassMethodIMP);类方法,Class cls传MetaClass class_addMethod(object_getClass(self)/[self getMetaClassWithChildClass:self]/,sel,(IMP)(dynamicClassMethodIMP), "@@:"); //OC语言写法:class_getMethodImplementation([self class], @selector(findSmokeMethod)) // class_addMethod(object_getClass(self),@selector(smoke),class_getMethodImplementation([self class], @selector(findSmokeMethod)),"@@:");

      //检测元类
      [self isMetaClass];
      
      return YES;
    

    } return [super resolveClassMethod:sel]; }

/** 判断是否是元类 */

  • (void)isMetaClass { /**class_isMetaClass 方法 通过 class_isMetaClass 方法可以验证判断是否是元类 */ Class c1 = object_getClass(self); Class c2 = [self getMetaClassWithChildClass:self]; BOOL object_getClass = class_isMetaClass(c1); BOOL objc_getMetaClass = class_isMetaClass(c2); NSLog(@"object_getClass是否是元类:%@",object_getClass?@"YES":@"NO"); NSLog(@"objc_getMetaClass是否是元类:%@",objc_getMetaClass?@"YES":@"NO"); }

/** 获取类的元类 @param childClass 目标类别 @return 返回元类 */

  • (Class)getMetaClassWithChildClass:(Class)childClass{ //转换字符串类别 const char * classChar = [NSStringFromClass(childClass) UTF8String]; //需要char的字符串 获取元类 return objc_getMetaClass(classChar); } 单独实现了一下类方法resolveClassMethod参考(仅仅写法不同)

#import "SuperMan.h" #import <objc/runtime.h> //包含对类、成员变量、属性、方法的操作

//C语言函数 void eat(id self,SEL sel){ NSLog(@"第一次转发:方法解析----类方法:eat"); }

@implementation SuperMan

  • (BOOL)resolveClassMethod:(SEL)sel { /** class: 给哪个类添加方法 SEL: 添加哪个方法 IMP: 方法实现 => 函数 => 函数入口 => 函数名 type: 方法类型:void用v来表示,id参数用@来表示,SEL用:来表示 */

//    Method exchangeM = class_getInstanceMethod([self class], @selector(eatWithPersonName:)); //    class_addMethod([self class], sel, class_getMethodImplementation(self, @selector(eatWithPersonName:)),method_getTypeEncoding(exchangeM));

if (sel == NSSelectorFromString(@"eat")) {
    //C语言函数写法:(IMP)eat
    class_addMethod(self, @selector(eat), (IMP)eat, "v@:");
    return YES;
} else if (sel == NSSelectorFromString(@"writeCode")) {
    NSLog(@"我在写代码");
    return YES;
}
return [super resolveClassMethod:sel];

}

@end class_addMethod 方法可参考文章iOS-Runtime之class_addMethod给类动态添加方法

这里有一个需要特别注意的地方,类方法需要添加到元类里面,OC中所有的类本质上来说都是对象,对象的isa指向本类,类的isa指向元类,元类的isa指向根元类,根元类的isa指向自己,这样的话就形成了一个闭环。元类方法以及判断是否为元类方法如下:

/** 判断是否是元类 */

  • (void)isMetaClass { /**class_isMetaClass 方法 通过 class_isMetaClass 方法可以验证判断是否是元类 */ Class c1 = object_getClass(self); Class c2 = [self getMetaClassWithChildClass:self]; BOOL object_getClass = class_isMetaClass(c1); BOOL objc_getMetaClass = class_isMetaClass(c2); NSLog(@"object_getClass是否是元类:%@",object_getClass?@"YES":@"NO"); NSLog(@"objc_getMetaClass是否是元类:%@",objc_getMetaClass?@"YES":@"NO"); }

/** 获取类的元类 @param childClass 目标类别 @return 返回元类 */

  • (Class)getMetaClassWithChildClass:(Class)childClass{ //转换字符串类别 const char * classChar = [NSStringFromClass(childClass) UTF8String]; //需要char的字符串 获取元类 return objc_getMetaClass(classChar); } 输出结果:

在MainViewController中创建Man对象,并且调用Man类和Person类中不存在的实例方法drinkPear和类方法smoke

Man *man = [[Man alloc]init]; //找不到实例方法 [man performSelector:@selector(drinkPear)];

//找不到类方法 [Man performSelector:@selector(smoke)]; 输出结果如下:

 当然,我们在MainViewController中去实现Man类的实例方法+ (BOOL)resolveInstanceMethod:(SEL)sel也是可以的,只是需要将class_addMethod方法中的Class类名更改为[Man class],且IMP imp 参数更改为class_getMethodImplementation([MainViewController class],且要在MainViewController中实现转发Man的实例方法

#import "MainViewController.h" #import "Man.h" #import "SuperMan.h" #import <objc/runtime.h> //包含对类、成员变量、属性、方法的操作 //#import <objc/message.h> 包含消息机制

@interface MainViewController ()

@end

@implementation MainViewController

  • (void)viewDidLoad { [super viewDidLoad];

    //第一次转发:方法解析 Method resolution [self FirsForward]; }

#pragma mark - 第一次转发:方法解析 Method resolution

  • (void)FirsForward { Man *man = [[Man alloc]init]; //找不到实例方法 [man performSelector:@selector(drinkPear)];

    //找不到类方法 [Man performSelector:@selector(smoke)];

    //找不到类方法 SuperMan *superMan = [[SuperMan alloc] init]; SEL select = NSSelectorFromString(@"eat"); [SuperMan resolveClassMethod:select]; [superMan performSelector:@selector(eat)]; }

  • (void)findDrinkPearMethod { NSLog(@"实例方法:Man drinkPear"); }

//实例方法

  • (BOOL)resolveInstanceMethod:(SEL)sel { if ([super resolveInstanceMethod:sel]) { return YES; }else { //IMP imp参数 OC写法:class_getMethodImplementation class_addMethod([Man class],@selector(drinkPear),class_getMethodImplementation([MainViewController class], @selector(findDrinkPearMethod)),"v@:"); return YES; } }

@end 输出结果:这时会执行MainViewController中resolveInstanceMethod方法,不会再执行Man类中的resolveInstanceMethod方法,如果这时候在MainViewController中处理找不到的方法,那么第二次、第三次转发都不会再走了。

在 MainViewController中和Man中都可以实现实例方法resolveInstanceMethod,但是类方法只能在Man类中实现

当Man 收到了未知 drinkPear(实例方法)和smoke(类方法)的消息的时候,实例方法会首选调用上文的resolveInstanceMethod:方法,类方法调用resolveClassMethod:方法,然后通过class_addMethod方法动态添加了一个drinkPear(实例方法)和smoke(类方法)的实现方法来解决掉这条未知的消息,此时消息转发过程提前结束。 但是当Man 未收到drinkPear(实例方法)和smoke(类方法) 这条未知消息的时候,第一步返回的是NO,也就是没有动态新增实现方法的时候就会调用第二次转发

  2、第二次转发:快速转发(消息重定向) (后面第二阶段、第三阶段都针对对象来处理,不考虑类方法)

如果第一次转发方法的实现没有被找到,那么会调用如下方法:- (id)forwardingTargetForSelector:(SEL)aSelector

  • (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); 这一步中,把消息转给其他对象replacement receiver;改变该 Selector的调用对象

这时开始进行消息快速转发,如果我们可以找到一个对象来实现调用的study方法,可以在这个方法里将这个对象返回,然后该对象就会执行我们调用的study方法,代码如下:

#pragma mark - 第二次转发:快速转发 Fast forwarding

  • (void)SecondForward { Man *man = [[Man alloc]init]; [man performSelector:@selector(study)]; } Man.m文件

#pragma mark - 第二次转发:快速转发 Fast forwarding

  • (id)forwardingTargetForSelector:(SEL)aSelector { NSLog(@"第二次转发:快速转发----forwardingTargetForSelector: %@", NSStringFromSelector(aSelector)); Son *son = [[Son alloc] init]; if ([son respondsToSelector: aSelector]) { return son; } return [super forwardingTargetForSelector: aSelector]; } 这时候会找到Son类调用其 study方法

Son.h文件 @interface Son : NSObject

//学习

  • (void)study;

@end

Son.m文件 @implementation Son

//学习

  • (void)study { NSLog(@"我要好好学习,天天向上!"); }

@end 输出结果如下:

  3、第三次转发:常规转发 Normal forwarding(消息转发) 如果第二次转发也没有找到可以处理方法的对象的话,那么会调用如下方法:

  • (void)forwardInvocation:(NSInvocation *)anInvocation;

  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

  • (void)doesNotRecognizeSelector:(SEL)aSelector; 消息转发也是改变调用对象,使该消息在新对象上调用;不同是forwardInvocation方法带有一个NSInvocation对象,这个对象保存了这个方法调用的所有信息,包括SEL,参数和返回值描述等,JSPatch就是基于消息转发实现的

当Man类 收到一条code的消息的时候,发现前两步都没办法处理掉,走到第三步:

这时Man类的

  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法就会被调用,这个方法会返回一个code的方法签名,

如果返回了code的方法签名的话,Man类的

  • (void)forwardInvocation:(NSInvocation *)anInvocation方法会被调用,在这个方法里处理我们调用的code方法,

如果这个方法里也处理不了的话,就会执行 doesNotRecognizeSelector方法,引起一个unrecognized selector sent to instance异常崩溃。

#pragma mark - 第三次转发:常规转发 Normal forwarding

  • (void)ThirdForward { Man *man = [[Man alloc]init]; [man performSelector:@selector(code)]; //三次转发都找不到的方法 [man performSelector:@selector(missMethod)]; } Man.m文件

#pragma mark - 第三次转发:常规转发 Normal forwarding //返回SEL方法的签名,返回的签名是根据方法的参数来封装的

  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSLog(@"第三次转发:常规转发----method signature for selector: %@", NSStringFromSelector(aSelector)); if (aSelector == @selector(code)) { return [NSMethodSignature signatureWithObjCTypes:"V@:@"]; } return [super methodSignatureForSelector:aSelector]; }

//拿到方法签名,并且处理(创建备用对象响应传递进来等待响应的SEL)

  • (void)forwardInvocation:(NSInvocation *)anInvocation { NSLog(@"forwardInvocation: %@", NSStringFromSelector([anInvocation selector])); if ([anInvocation selector] == @selector(code)) { //创建备用对象 CodeMan *codeMan = [[CodeMan alloc] init]; //备用对象响应传递进来等待响应的SEL [anInvocation invokeWithTarget:codeMan]; } }

// 如果备用对象不能响应 则抛出异常

  • (void)doesNotRecognizeSelector:(SEL)aSelector { NSLog(@"doesNotRecognizeSelector: %@", NSStringFromSelector(aSelector)); [super doesNotRecognizeSelector:aSelector]; } missMethod方法处理不了,走了doesNotRecognizeSelector方法,如下

这时候会找到CodeMan类调用其 code方法

CodeMan.h @interface CodeMan : NSObject

//编码

  • (void)code;

@end

CodeMan.m @implementation CodeMan

//编码

  • (void)code { NSLog(@"我要学习编程!"); }

@end 输出结果如下:

此时这个code消息已经被codeMan实例处理掉

在三次转发阶段的每一阶段,消息接受者都有机会去处理消息。越往后面阶段处理代价越高,最好的情况是在第一阶段就处理消息,这样runtime会在处理完后缓存结果,下回再发送同样消息的时候,可以提高处理效率。第二阶段转移消息的接受者也比进入转发流程的代价要小,如果到最后一步forwardInvocation的话,就需要处理完整的NSInvocation对象了。

此时消息转发流程完整的结束了,完整的消息转发流程如下:

四、实际用途 摘选自iOS Runtime 消息转发机制原理和实际用途

1、JSPatch --iOS动态化更新方案 具体实现bang神已经在下面两篇博客内进行了详细的讲解,非常精妙的使用了,消息转发机制来进行JS和OC的交互,从而实现iOS的热更新。虽然2017年苹果大力整改热更新让JSPatch的审核通过率在有一段时间里面无法过审,但是后面bang神对源码进行代码混淆之后,基本上是可以过审了。不论如何,这个动态化方案都是技术的一次进步,不过目前是被苹果爸爸打压的。不过如果在bang神的平台上用正规混淆版本别自己乱来,通过率还是可以的。有兴趣的同学可以看看这两篇原理文章,这里只摘出来用到消息转发的部分。

blog.cnbang.net/tech/2808/ blog.cnbang.net/tech/2855/

 

具体的实现原理可以去bang神的博客查看。

2、为 @dynamic 实现方法 使用 @synthesize 可以为 @property 自动生成 getter 和 setter 方法(现 Xcode 版本中,会自动生成),而 @dynamic 则是告诉编译器,不用生成 getter 和 setter 方法。当使用 @dynamic 时,我们可以使用消息转发机制,来动态添加 getter 和 setter 方法。当然你也用其他的方法来实现。

3、实现多重代理 利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。

blog.csdn.net/kingjxust/a…

4、间接实现多继承 Objective-C本身不支持多继承,这是因为消息机制名称查找发生在运行时而非编译时,很难解决多个基类可能导致的二义性问题,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。

 

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

参考文章:

iOS Runtime 消息转发机制原理和实际用途   

iOS理解Objective-C中消息转发机制附Demo

iOS消息转发机制及避免崩溃的解决方案 吃大米的小蚂蚁 

————————————————

版权声明:本文为CSDN博主「MinggeQingchun」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/MinggeQingc…