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对象了。

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

四、实际用途

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

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 消息转发机制原理和实际用途   

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

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

以上是关于iOS-Runtime消息发送转发机制的主要内容,如果未能解决你的问题,请参考以下文章

ios-Runtime机制

IOS-Runtime(消息机制)

(转)iOS-Runtime知识点整理

iOS-Runtime的那些事...编辑中....

揭秘 Objective-C Runtime - 消息转发

.Net Core&RabbitMQ消息转发可靠机制(上)