iOS-Runtime消息发送转发机制
Posted MinggeQingchun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS-Runtime消息发送转发机制相关的知识,希望对你有一定的参考价值。
可在GitHub上下载示例源代码Demo ,欢迎点赞给星,谢谢!
Runtime消息转发机制
1、Runtime ; 官方文档Objective-C Runtime Programming Guide
Runtime简称运行时,使OC语言具有动态的特性。
Runtime的运行机制使得OC能够在运行时动态创建类和对象, 进行消息传递转发等
运行时, 编译时区别
编译时: 其实就是正在编译的时候, 编译器帮你把源代码翻译成机器代码的过程。
主要是对语言进行最基本的检查报错(词法分析, 语法分析等)。例如: 编译代码时候如果有error, warning信息, 都是编译器检查出来的, 这种错误叫编译时错误, 这个过程叫编译时类型检查 或者静态类型检查。
留意: 编译时只是把代码当做文本扫描下, 并未分配内存运行
运行时: 是代码跑起来, 被装载到内存中的过程(内存中判断),是一个动态阶段。
Runtime 是 OC底层的一套C语言的API(引入 <objc/runtime.h>
或<objc/message.h>
),<objc/runtime.h> //包含对类、成员变量、属性、方法的操作;
<objc/message.h> //包含消息机制
编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:
clang -rewrite-objc xxx.m
这个命令会报错:
In file included from ViewController.m:8:
./ViewController.h:8:9: fatal error: 'UIKit/UIKit.h' file not found
#import <UIKit/UIKit.h>
^~~~~~~~~~~~~~~
1 error generated.
找不到 'UIKit/UIKit.h' file文件,因此我们需要执行新的命令
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk xxx.m
可以看到编译后的xxx.cpp(C++文件),打开查看代码:
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"));
我们可以看到调用方法本质就是objc_msgSend发消息,[[NSObject alloc]init]
语句发了两次消息,第一次发了alloc 消息,第二次发送init 消息。
我们也可以验证一下
1、调用objc_msgSend,需要导入头文件#import <objc/message.h>
2、TARGET → Build Settings → 搜索 msg → Enable Strict Checking of objc_msgSend Calls由YES 改为NO
将严厉的检查机制关掉,如果Enable Strict Checking of objc_msgSend Calls还是YES,则objc_msgSend的参数会报错
Too many arguments to function call, expected 0, have 2
测试代码:
//obc_msgSend 方法
- (void)testObcMsgSend {
Person *person = [Person new];
[person eat];
objc_msgSend(person,sel_registerName("eat"));
}
输出相同; [person eat];等同于objc_msgSend(person,sel_registerName("eat"));
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(消息转发)
如果第二次转发也没有找到可以处理方法的对象的话,那么会调用如下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (void)doesNotRecognizeSelector:(SEL)aSelector;
消息转发也是改变调用对象,使该消息在新对象上调用;不同是forwardInvocation
方法带有一个NSInvocation
对象,这个对象保存了这个方法调用的所有信息,包括SEL
,参数和返回值描述等,JSPatch
就是基于消息转发实现的
当Man类 收到一条code的消息的时候,发现前两步都没办法处理掉,走到第三步:
这时Man类的
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法就会被调用,这个方法会返回一个code的方法签名,
如果返回了code的方法签名的话,Man类的
- (void)forwardInvocation:(NSInvocation *)anInvocation方法会被调用,在这个方法里处理我们调用的code方法,
如果这个方法里也处理不了的话,就会执行
- (void)doesNotRecognizeSelector:(SEL)aSelector方法,引起一个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对象了。
此时消息转发流程完整的结束了,完整的消息转发流程如下:
四、实际用途
1、JSPatch --iOS动态化更新方案
具体实现bang神已经在下面两篇博客内进行了详细的讲解,非常精妙的使用了,消息转发机制来进行JS和OC的交互,从而实现ios的热更新。虽然2017年苹果大力整改热更新让JSPatch的审核通过率在有一段时间里面无法过审,但是后面bang神对源码进行代码混淆之后,基本上是可以过审了。不论如何,这个动态化方案都是技术的一次进步,不过目前是被苹果爸爸打压的。不过如果在bang神的平台上用正规混淆版本别自己乱来,通过率还是可以的。有兴趣的同学可以看看这两篇原理文章,这里只摘出来用到消息转发的部分。
http://blog.cnbang.net/tech/2808/
http://blog.cnbang.net/tech/2855/
具体的实现原理可以去bang神的博客查看。
2、为 @dynamic 实现方法
使用 @synthesize 可以为 @property 自动生成 getter 和 setter 方法(现 Xcode 版本中,会自动生成),而 @dynamic 则是告诉编译器,不用生成 getter 和 setter 方法。当使用 @dynamic 时,我们可以使用消息转发机制,来动态添加 getter 和 setter 方法。当然你也用其他的方法来实现。
3、实现多重代理
利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。
https://blog.csdn.net/kingjxust/article/details/49559091
4、间接实现多继承
Objective-C本身不支持多继承,这是因为消息机制名称查找发生在运行时而非编译时,很难解决多个基类可能导致的二义性问题,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。
可在GitHub上下载示例源代码Demo ,欢迎点赞给星,谢谢!
参考文章:
以上是关于iOS-Runtime消息发送转发机制的主要内容,如果未能解决你的问题,请参考以下文章