浅谈系列-OC消息机制是如何运行的

1,890 阅读6分钟

浅谈系列-OC对象创建出来到底是怎么样的呢

浅谈系列-OC方法调用到底是个什么流程

浅谈系列-class 、meta-class结构简单理解

导语:通过对OC对象,类对象,元类对象结构的一些简单理解,方法调用流程的了解,我们可以更简单看懂消息机制的发送消息阶段是怎么样搜索方法的。

OC的消息机制

  • OC中的方法调用有别于其他语言的函数调用机制(地址传递调用函数),是通过消息机制,所有的方法调用都会在Runtime运行期通过objc_msgsend这个函数完成。

比如,[person eatWithFood:@[@"rice",@"fruits"]]

可以理解为系统给person这个对象发送了一条eatWithFood消息。

让person找到对应方法实现,执行其代码。

  • 系统调用objc_msgSend(id self, SEL, cmd, ...)
  1. 会把person作为函数的第一个参数,称为消息的“接收者
  2. eatWithFood这个方法会作为第二个参数,可以称为“选择子
  3. eatWithFood方法的参数做为objc_msgSend函数后面的参数按顺序传入
  • objc_msgSend在底层分三个阶段执行:
  1. 消息发送(在当前类对象,父类类对象中查找对应方法)
  2. 动态方法解析
  3. 消息转发

消息发送

  • 首先,在接收者所属类中查找对应方法,如果查不到就按照继承体系往上查找,找到该方法后跳至代码实现。
//class:类对象  meta-class:元类对象
对象方法查找路径:class -> super class -> ...-> root class
类方法查找路径:meta-class -> super meta-class ->...-> root meta-class -> root class
方法查找具体步骤
1. 在类的方法缓存cache中查找,有就调用,没有就在本类的class_rw_t中查找,有就调用,并且会把方法缓存到cache中方便下次查找。
2. 如果还是没有找到,就到父类中重复同样的操作,只要查找到方法都会在调用方法并且把方法缓存到自己类的cache中。
3. 如果在执行完消息发送,还是没有找到方法就进入动态方法解析阶段。

动态方法解析:

  • 经过消息发送并没查找到对应方法,Runtime就会征询接收者,是否可以动态添加方法,处理当前选择子,这个阶段就是“动态方法解析”。

1.对象在收到无法解读的消息后,会先调用类方法

+ (BOOL)resolveInstanceMethod:(SEL)sel
参数sel:就是选择子,未查找到的方法

表示询问是否可以新增一个方法处理此选择子。

2.添加方法可以使用

class_addMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name, IMP  _Nonnull imp, const char * _Nullable types)
参数1:cls  就是消息接收者
参数2:name 就是sel选择子(找不到实现的方法)
参数3:imp 代表函数指针(动态添加的方法)
参数4:types 就是Type Encoding,字符串编码
  • 例子:
//- (void)eatWithFood:(NSArray *)foods{
//    NSLog(@"I eated  %@ for lunch",foods);
//}

#pragma mark - 动态方法解析

//method_t是底层结构体,并没有暴露出来不可直接使用,我们模仿底层写一个
struct method_t{
    SEL sel;
    char *types;
    IMP imp;
};

+ (BOOL)resolveInstanceMethod:(SEL)sel{

    if (@selector(eatWithFood:) == sel) {
     
        //1.通过模仿底层的method_t强转来做方法添加
//        //强转
//        struct method_t *newMethod = (struct method_t *)class_getInstanceMethod(self, @selector(eatLuch:));
//
//        //新添加方法
//        class_addMethod(self, sel, newMethod->imp, newMethod->types);
        
        //2.调用系统现成接口
//        Method method = class_getInstanceMethod(self, @selector(eatLuch:));
//
//        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        
        
        //3.调用现成接口(3跟2不同的是直接获取了imp)
        
        IMP imp = class_getMethodImplementation(self, @selector(eatLuch:));
        
        Method method = class_getInstanceMethod(self, @selector(eatLuch:));
        
        const char *types = method_getTypeEncoding(method);
        
        class_addMethod(self, sel, imp, types);
        
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//添加的方法实现
- (void)eatLuch:(NSArray *)foods{
    NSLog(@"午饭吃了%@", foods);
}

  • 也可以添加c语言函数实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(eatWithFood:)) {
    
        class_addMethod(self, sel, (IMP)eatSomeFoods, "v24@0:8@16");
        
    }
    
    return [super resolveInstanceMethod:sel];
}

//c语言函数
void eatSomeFoods(id self, SEL _cmd, NSArray *foods){
    NSLog(@"午饭吃了---%@",foods);
}

  • 以上的动态方法解析,都是针对对象方法举例的,如果是类方法,调用以下方法
+ (BOOL)resolveClassMethod:(SEL)sel

消息转发

  • 如果消息接收者没有允许动态添加方法,就进入到消息转发阶段,此阶段也分为两个阶段:

1.请接收者查看有没有其他对象处理该方法,若有就转发消息给该对象处理。

2.如果没其他对象处理消息,就启动完整的消息转发机制,runtime会把与此消息有关的所有细节都封装到NSInvocation对象中,给接收者最后一次处理数据的机会,令其设法处理该消息。

备援接收者

如果征询到类无法动态添加方法,就会请接收者查看是否有其他类来处理该选择子,如果有这个对象就是备援接收者

该步骤系统会调用以下方法来处理:

- (id)forwardingTargetForSelector:(SEL)aSelector
只要返回一个可以处理方法的对象就可以了

例如:

FRPerson.m


- (id)forwardingTargetForSelector:(SEL)aSelector{
    
    //直接返回一个可以处理该选择子的对象,然后在该对象内部实现该方法
    if (aSelector == @selector(eatWithFood:)) {
    
        return [[FRStudent alloc] init];
        
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

FRStudent.m

#import "FRStudent.h"

@implementation FRStudent
- (void)eatWithFood:(NSArray *)foods{
    NSLog(@"学生午饭吃了 %@ ",foods);
}
@end

在本例子中,FRStudent处理了消息,消息转发到此为止。

完整的消息转发机制
  1. 如果没有其他接收者处理消息,进入完整的消息转发机制
  2. 会把所有相关信息封装到NSInvocation类中
  3. 通过forwardInvocation方法发给目标对象
  • 系统会调用以下方法:
//返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
1.如果方法签名返回nil,就表明放弃最后处理方法的机会,系统抛出错误
2.返回了方法签名之后,触发forwardInvocation方法
//触发forwardInvocation之后,可以在此方法实现里做任何操作
- (void)forwardInvocation:(NSInvocation *)anInvocation

例如:


//返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
    if (aSelector == @selector(eatWithFood:)) {
        
        return [NSMethodSignature signatureWithObjCTypes:"v24@0:8@16"];
    }
    
    return  nil;
}

//methodSignatureForSelector方法返回nil,forwardInvocation就不会被调用
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    
    if (anInvocation.selector == @selector(eatWithFood:)) {
        
        //1.可以转发个多个对象处理消息
//        [anInvocation invokeWithTarget:[[NSClassFromString(@"FRStudent") alloc] init]];
//
//        [anInvocation invokeWithTarget:[[NSClassFromString(@"FRCat") alloc] init]];
        
        //2.可以修改target调用者,也就是改变对象处理消息
//        anInvocation.target = NSClassFromString(@"FRStudent");
//        [anInvocation invoke];
        
        //3.来到这个方法之后也可以不处理那个没有实现的方法,可以做其他与这个方法毫无关系的其他事情,还可以什么都不做
        
    }
}

  • 注意:进入方法转发阶段的三个方法都有类方法类型,用来处理调用类方法。
+ (id)forwardingTargetForSelector:(SEL)aSelector
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
+ (void)forwardInvocation:(NSInvocation *)anInvocation

如果xcode没有提示,可以先敲出对象方法,把“-”改为“+”就好。

浅谈系列 - KVO&KVC到底是怎么样实现的

浅谈系列-分类的结构和加载时机

浅谈系列-load 和 initialize的调用时机和实际运用

浅谈系列-block如何工作的