美文网首页
IOS 消息传递与消息转发

IOS 消息传递与消息转发

作者: 不吃香菜11 | 来源:发表于2021-04-08 21:36 被阅读0次

    1、方法method和selector(选择子)有什么关系

    在 Objective-C 中,selector,Method 和 implementation(IMP) 都是 Runtime 的组成部分。在实际开发中它们常常是可以相互转换来处理消息的发送的。选择子代表方法在 Runtime 期间的标识符。为 SEL 类型,虽然 SEL 是 objc_selector 结构体指针,但实际上它只是一个 C 字符串。在类加载的时候,编译器会生成与方法相对应的选择子,并注册到 Objective-C 的 Runtime 运行系统。
    得出结论:选择子其实是方法的名称,不同类中方法名相同参数不同的俩个方法,他们的选择子是相同的。

    Method的结构体

    /// Method
    struct objc_method {
        SEL method_name; 
        char *method_types;
        IMP method_imp;
    };
    
    • 方法名 method_name 类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
    • 方法类型 method_types 是个 char 指针,其实存储着方法的参数类型和返回值类型,即是 Type Encoding 编码。(即类型编码)
    • method_imp 指向方法的实现,本质上是一个函数的指针,就是前面讲到的 Implementation。

    消息传递

    在OC中,给对象发送消息的语法是:

    id returnValue = [someObject messageName:parameter];
    

    这里,someObject叫做接收者(receiver),messageName:叫做选择子(selector),选择子和参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数叫做objc_msgSend,它的原型如下:

    void objc_msgSend(id self, SEL cmd, ...)
    

    第一个参数代表接收者,第二个参数代表选择子,后续参数就是消息中的那些参数,数量是可变的·,所以这个函数就是参数个数可变的函数。

    因此,上述以OC形式展现出来的函数就会转化成如下函数:

    id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
    

    可以看出,在调用方法时,编译器将它转成了objc_msgSend消息发送了,在Runtime的执行过程如下

    • 1、Runtime先通过对象someobject找到isa指针,判断isa指针是否为nil,为nil直接return。
    • 2、若不为空则通过isa指针找到当前实例的类对象,在类对象下查找缓存是否有messageName方法。
    • 3、若在类对象缓存中找到messageName方法,则直接调用IMP方法(本质上是函数的指针)。
    • 4、若在类对象缓存中没找到messageName方法,则查找当前类对象的方法列表methodlist,若找到方法则将其添加到类对象的缓存中。
    • 5、若在类对象方法列表中没找到messageName方法,则继续到当前类的父类中以相同的方式查找(即类的缓存->类的方法列表)。
    • 6、若在父类中找到messageName方法,则将IMP添加到类对象缓存中。
    • 7、若在父类中没找到messageName方法,则继续查询父类的父类,直到追溯到最上层NSObject
    • 8、若还是没有找到,则启用动态方法解析、备用接收者、消息转发三部曲,给程序最后一个机会
    • 9、若还是没找到,则Runtime会抛出异常doesNotRecognizeSelector

    综上,方法的查询流程基本就是查询类对象中的缓存和方法列表->父类中的缓存和方法列表->父类的父类中的缓存和方法列表->...->NSObject中的缓存和方法列表->动态方法解析->备用接收者->消息转发

    消息动态解析

    Objective-C 运行时会调用 +resolveInstanceMethod: 或者 +resolveClassMethod:,让你有机会提供一个函数实现。前者在 对象方法未找到时 调用,后者在 类方法未找到时 调用。我们可以通过重写这两个方法,添加其他函数实现,并返回 YES, 那运行时系统就会重新启动一次消息发送的过程。

    主要用的的方法如下:

    // 类方法未找到时调起,可以在此添加方法实现
    + (BOOL)resolveClassMethod:(SEL)sel;
    // 对象方法未找到时调起,可以在此添加方法实现
    + (BOOL)resolveInstanceMethod:(SEL)sel;
    
    /** 
     * class_addMethod    向具有给定名称和实现的类中添加新方法
     * @param cls         被添加方法的类
     * @param name        selector 方法名
     * @param imp         实现方法的函数指针
     * @param types imp   指向函数的返回值与参数类型
     * @return            如果添加方法成功返回 YES,否则返回 NO
     */
    BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                    const char * _Nullable types);
    

    测试代码

    main.m 文件中
      int main(int argc, const char * argv[]) {
        @autoreleasepool {
        }
        Person *xiaoming = [[Person alloc]init];
        [xiaoming performSelector:@selector(way)];
        return 0;
        
    }
    
    person.m 文件中
      // 重写 resolveInstanceMethod: 添加对象方法实现
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        if (sel == @selector(way)) {
            class_addMethod([self class], sel,class_getMethodImplementation([self class], @selector(method)), "123");//使用class_addMethod动态添加方法method
        }
        return YES;
    }
    - (void)method{
        NSLog(@"进入了消息动态解析");
    }
    
    运行了上述的测试代码后,我们会发现即便我们并没有实现way方法,而且使用了performSelector去强行调用way方法,但是我们的程序并没有崩溃,是因为在查找了类方法和所有的父类后还是没有找到way方法,程序进入了消息动态解析,然后我们使用了class_addMethod去动态添加方法method,最后程序从调用
    

    performSelector和直接调用方法的区别

    performSelector: withObject:是在iOS中的一种方法调用方式。他可以向一个对象传递任何消息,而不需要在编译的时候声明这些方法。所以这也是runtime的一种应用方式。

    所以performSelector和直接调用方法的区别就在与runtime。直接调用编译是会自动校验。如果方法不存在,那么直接调用 在编译时候就能够发现,编译器会直接报错。
    但是使用performSelector的话一定是在运行时候才能发现,如果此方法不存在就会崩溃。所以一般使用performSelector的时候,一般都会使用- (BOOL)respondsToSelector:(SEL)aSelector;来在运行时判断对象是否响应此方法。

    消息接受者重定向(备用接受者)

    如果上一步中 +resolveInstanceMethod: 或者 +resolveClassMethod: 没有添加其他函数实现,运行时就会进行下一步:消息接受者重定向。

    如果当前对象实现了 -forwardingTargetForSelector: 或者 +forwardingTargetForSelector: 方法,Runtime 就会调用这个方法,允许我们将消息的接受者转发给其他对象。

    其中用到的方法。

    // 重定向类方法的消息接收者,返回一个类或实例对象
    + (id)forwardingTargetForSelector:(SEL)aSelector;
    // 重定向方法的消息接收者,返回一个类或实例对象
    - (id)forwardingTargetForSelector:(SEL)aSelector;
    

    注意:

    1. 类方法和对象方法消息转发第二步调用的方法不一样,前者是+forwardingTargetForSelector: 方法,后者是 -forwardingTargetForSelector: 方法。
    2. 这里+resolveInstanceMethod: 或者 +resolveClassMethod:无论是返回 YES,还是返回 NO,只要其中没有添加其他函数实现,运行时都会进行下一步。

    测试代码

    - (id)forwardingTargetForSelector:(SEL)aSelector{
        if (aSelector == @selector(way)) {
            Friends *friends = [[Friends alloc]init];
            return friends;//返回friends对象,让friends对象接受这个消息
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    

    可以看到,虽然当前 person 没有实现 fun 方法,+resolveInstanceMethod: 也没有添加其他函数实现。但是我们通过 forwardingTargetForSelector 把当前 person 的方法转发给了 friends 对象去执行了。打印结果也证明我们成功实现了转发。

    我们通过 forwardingTargetForSelector 可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是 nil,也不是 self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步:消息重定向流程。

    消息重定向

    如果经过消息动态解析、消息接受者重定向,Runtime 系统还是找不到相应的方法实现而无法响应消息,Runtime 系统会利用 -methodSignatureForSelector: 或者 +methodSignatureForSelector: 方法获取函数的参数和返回值类型。

    • 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
    • 如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。

    所以我们可以在 forwardInvocation: 方法中对消息进行转发。

    注意:类方法和对象方法消息转发第三步调用的方法同样不一样。
    类方法调用的是:

    1. + methodSignatureForSelector:
    2. + forwardInvocation:
    3. + doesNotRecognizeSelector:

    对象方法调用的是:

    1. - methodSignatureForSelector:
    2. - forwardInvocation:
    3. - doesNotRecognizeSelector:

    用到的方法:

    // 获取类方法函数的参数和返回值类型,返回签名
    + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    
    // 类方法消息重定向
    + (void)forwardInvocation:(NSInvocation *)anInvocation;
    
    // 获取对象方法函数的参数和返回值类型,返回签名
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    
    // 对象方法消息重定向
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        if (aSelector == @selector(way)) {
            return [NSMethodSignature methodSignatureForSelector:@selector(way)];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    - (void)forwardInvocation:(NSInvocation *)anInvocation{
        SEL sel = anInvocation.selector;
        Friends *f = [[Friends alloc] init];
        if([f respondsToSelector:sel]) {   // 判断 Person 对象方法是否可以响应 sel
            [anInvocation invokeWithTarget:f];  // 若可以响应,则将消息转发给其他对象处理
        } else {
            [self doesNotRecognizeSelector:sel];  // 若仍然无法响应,则报错:找不到响应方法
        }
    }
    

    既然 -forwardingTargetForSelector:-forwardInvocation: 都可以将消息转发给其他对象处理,那么两者的区别在哪?

    区别就在于 -forwardingTargetForSelector: 只能将消息转发给一个对象。而 -forwardInvocation: 可以将消息转发给多个对象。

    以上就是 Runtime 消息转发的整个流程。

    消息发送以及转发机制总结

    调用 [receiver selector]; 后,进行的流程:

    1. 编译阶段:

      [receiver selector]; 方法被编译器转换为:

      1. objc_msgSend(receiver,selector) (不带参数)
      2. objc_msgSend(recevier,selector,org1,org2,…)(带参数)
    2. 运行时阶段:消息接受者

      recevier寻找对应的 selector

      1. 通过 recevierisa 指针 找到 recevierclass(类)
      2. Class(类)cache(方法缓存) 的散列表中寻找对应的 IMP(方法实现)
      3. 如果在 cache(方法缓存) 中没有找到对应的 IMP(方法实现) 的话,就继续在 Class(类)method list(方法列表) 中找对应的 selector,如果找到,填充到 cache(方法缓存) 中,并返回 selector
      4. 如果在 class(类) 中没有找到这个 selector,就继续在它的 superclass(父类)中寻找;
      5. 一旦找到对应的 selector,直接执行 recevier 对应 selector 方法实现的 IMP(方法实现)
      6. 若找不到对应的 selector,Runtime 系统进入消息转发机制。
    3. 运行时消息转发阶段:

      1. 动态解析:通过重写 +resolveInstanceMethod: 或者 +resolveClassMethod:方法,利用 class_addMethod方法添加其他函数实现;
      2. 消息接受者重定向:如果上一步添加其他函数实现,可在当前对象中利用 forwardingTargetForSelector: 方法将消息的接受者转发给其他对象;
      3. 消息重定向:如果上一步没有返回值为 nil,则利用 methodSignatureForSelector:方法获取函数的参数和返回值类型。
        1. 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。
        2. 如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。

    相关文章

      网友评论

          本文标题:IOS 消息传递与消息转发

          本文链接:https://www.haomeiwen.com/subject/tcqbkltx.html