美文网首页Swift iOS Developer
iOS 消息转发机制(依据实例展开理论)

iOS 消息转发机制(依据实例展开理论)

作者: Mccc_ | 来源:发表于2019-12-25 17:53 被阅读0次

    先总结,后解释

    Objective-C当向一个对象发送消息时,寻找消息的顺序
    1.寻找类自身的方法实现

    先会调用objc_msgSend方法,首先在Class中的缓存和方法列表中查找IMP。

    2.寻找父类的方法实现

    如果该类中没有找到,则向父类的Class查找。如果一直查找到根类仍旧没有找到,则执行消息转发

    3. 动态添加模式

    调用resolveInstanceMethod:resolveClassMethod:方法。允许用户在此时为该Class动态添加实现方法。如过实现了,调用并返回YES,重新开始objc_msgSend流程。如果仍没有实现,继续下面的动作。

    4.快速向前转发模式

    调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,并返回非nil对象。否则返回nil,继续下面的动作。

    5.正常向前转发模式

    调用methodSignatureForSelector:方法,尝试获得一个方法签名。
    如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。
    如果能获取,则返回非nil并调用forwardInvocation:方法,将获取到的方法签名包装成Invocation传入。在forwardInvocation:内指定消息接收者来处理消息(如果不指定也不会报错了)。

    6.异常处理

    调用doesNotRecognizeSelector抛出异常。重写doesNotRecognizeSelector也可自定义异常的抛出。

    通过一个示例来了解消息转发机制

    1. 实现Animal
      只在.h中声明了方法,不在.m中实现该方法。
    #import <Foundation/Foundation.h>
    
    @interface Animal : NSObject
    - (void)eatTogetherWith:(Animal *)animal;
    @end
    
    #import "Animal.h"
    @implementation Animal
    @end
    
    1. ViewController中实现Animal并调用该方法。
    #import "ViewController.h"
    #import "Animal.h"
    
    @interface ViewController ()
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        Animal *one = [[Animal alloc] init];
        Animal *two = [[Animal alloc] init];
        [one eatTogetherWith:two];
    }
    @end
    
    1. 此时会崩溃,崩溃信息为
      Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Animal eatTogetherWith:]: unrecognized selector sent to instance 0x600001fd3280'

    这个过程,系统做了什么呢?

    向对象发送某个消息的时候,编译器会调用底层的 obj_msgSend() C函数,从缓存和方法列表中,寻找对象的函数指针(IMP),如果找到,则执行。否则继续根据一定的策略或者方式继续找,最终如果没有找到,则让程序 Crash, 报一个异常。

    objc_msgSend的作用是向一个实例类发送一个带有简单返回值的message,是一个参数个数不定的函数。当遇到一个方法调用,编译器会生成一个objc_msgSend的调用,有:objc_msgSend_stret、objc_msgSendSuper或者是objc_msgSendSuper_stret。发送给父类的message会使用objc_msgSendSuper,其他的消息会使用objc_msgSend。如果方法的返回值是一个结构体(structures),那么就会使用objc_msgSendSuper_stret或者objc_msgSend_stret。 第一个参数是:指向接收该消息的类的实例的指针;第二个参数是:要处理的消息的selector; 其他的就是要传入的参数。这样消息派发系统就在接收者所属类中查找器方法列表,如果找到和选择器名称相符的方法就跳转其实现代码,如果找不到,就再其父类找,等找到合适的方法在跳转到实现代码。这里跳转到实现代码这一操作利用了尾递归优化。 如果该消息无法被该类或者其父类解读,就会开始进行消息转发

    1. 从缓存和方法列表中,寻找对象的函数指针(IMP)

    当编译器看到这条消息的时候,就会转换为一条标准C函数:id objc_msgSend(id self, SEL _cmd, …),此时会将[one eatTogetherWith:two]变为:
    objc_msgSend(one,@selector(eatTogetherWith:),two)

    • 查看缓存中是否有匹配的函数指针(IMP),如果有则执行,否则继续。
    • 查看方法列表中是否有匹配的函数指针(IMP), 如果有则执行,否则继续。
    • 查看父类中是否有匹配的函数指针(IMP), 如果有则执行,否则继续。

    最终如果没有找到,则让程序 Crash, 报一个异常:unrecognized selector sent to instance

    2. 系统第一次挽救Crash:动态添加模式

    系统会查询该类中是否有动态方法解析。如果有则执行,否则继续。
    需要重写对应的方法实现动态方法解析

    // 在 Animal.m中
    /// 如果是实例方法,就重写该方法
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        NSString *method = NSStringFromSelector(sel);
        if ([method isEqualToString:@"eatTogetherWith:"]) {
            class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:@");
        }
        return [super resolveInstanceMethod:sel];
    }
    
    /// 如果是类方法,就重写该方法
    + (BOOL)resolveClassMethod:(SEL)sel {
        NSString *method = NSStringFromSelector(sel);
        if ([method isEqualToString:@"eatTogetherWith:"]) {
            class_addMethod(self, sel, (IMP)dynamicMethodIMP, "@@:");
        }
        return [super resolveClassMethod:sel];
    }
    
    /**
    要动态绑定的方法
     
    @param self 要绑定方法的对象
    @param _cmd 方法信息
    @param value 方法参数
    */
    void dynamicMethodIMP(id self,SEL _cmd,id value) {
        NSString *sel = NSStringFromSelector(_cmd);
        NSLog(@"self = %@ _cmd = %@ value = %@", self, sel, value);
    }
    
    // 打印结果: self = <Animal: 0x6000019a2d40> _cmd = eatTogetherWith: value = <Animal: 0x6000019a2d30>
    

    当消息传递无法处理的时候,首先会看一下所属类,是否动态添加了方法,以处理当前未知的选择子。这个过程叫做“动态方法解析”(dynamic method resolution)

    说明:

    • 该方法是实例方法时调用resolveInstanceMethod :,该方法是类方法时调用resolveClassMethod :
    • v@:@的含义(依次)
      • v: 表示返回类型是void
      • @:表示id (self receiver)
      • 冒号:表示SEL
      • @:方法的具体参数
    • 动态方法解析确切的说还不属于消息转发的过程,是在消息转发之前对实例方法或类方法进行补救。

    3. 第二次挽救Crash:快速向前转发模式

    快速消息转发也叫备援接收者/消息重定向,如果有指定消息接收对象则将消息转由接收对象响应,否则继续。

    新增一个Dog类,并实现一个同名的方法

    #import <Foundation/Foundation.h>
    #import "Animal.h"
    @interface Dog : NSObject
    - (void)eatTogetherWith:(Animal *)animal;
    @end
    
    #import "Dog.h"
    @implementation Dog
    - (void)eatTogetherWith:(Animal *)animal {
        NSLog(@"Dog类实现了eatTogetherWith方法");
    }
    @end
    

    在Animal里面添加以下代码,并注释掉动态方法解析的代码。

    - (id)forwardingTargetForSelector:(SEL)aSelector {
        NSString *method = NSStringFromSelector(aSelector);
        if ([method isEqualToString:@"eatTogetherWith:"]) {
            Dog *dog = [[Dog alloc] init];
            return dog;
        }
        return nil;
    }
    // 输出: Dog类实现了eatTogetherWith方法
    

    此时Animal类的eatTogetherWith:方法就通过快速消息转发模式转给了Dog类处理了。

    4. 第三次挽救崩溃:正常向前转发模式

    动态添加模式快速向前转发模式都没处理消息的话,会执行** 正常向前转发模式**。
    如果有指定转发对象则转发给该对象响应,否则抛出异常。

    先实现方法签名(注释掉上面两次的挽救代码)
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSString *methodName = NSStringFromSelector(aSelector);
        if ([methodName isEqualToString:@"eatTogetherWith:"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    指定消息接收者
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        SEL sel = [anInvocation selector];
        Dog *dog = [[Dog alloc] init];
        if ([dog respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:dog];
            return;
        }
        [super forwardInvocation:anInvocation];
    }
    

    调用methodSignatureForSelector:方法,尝试获得一个方法签名。
    如果获取不到签名,则直接调用doesNotRecognizeSelector:抛出异常。
    如果能获取到,则返回非nil并调用forwardInvocation:方法,将获取到的方法签名包装成Invocation传入。在forwardInvocation:内指定消息接收者来处理消息。
    此时不指定消息接收者也不会报错了。

    5. 抛出异常

    如果三次都没拯救,就调doesNotRecognizeSelector, 默认的实现是抛出异常。如果想更改异常内容可以重写该方法。

    - (void)doesNotRecognizeSelector:(SEL)aSelector {
        NSString *method = NSStringFromSelector(aSelector);
        if ([method isEqualToString:@"eatTogetherWith:"]) {
            NSLog(@"Animal 无法执行 eatTogetherWith:方法,特抛出异常告知。");
        }
    }
    



    延伸

    1. 什么是消息?

    崩溃的原因是执行了[one eatTogetherWith:two]方法,Animal类或者其父类中没有找到[Animal eatTogetherWith:]这个方法。

    • one叫做消息接收者
    • eatTogetherWith叫选择器
    • two是参数
    • 消息 = 选择器+参数

    2. SEL和IMP是什么?

    [one eatTogetherWith:two];可以换成
    [one performSelector:@selector(eatTogetherWith:) withObject:two];
    两者作用相同,都是向one这个实力发送一条eatTogetherWith:的消息,参数都是two。
    这里的@selector(eatTogetherWith:) 是消息的选择器或者选择子。

    • SEL:编译过程中,会根据方法的名字生成类型是 SEL的唯一 ID。通过方法名字(NSString)可以找到ID。
    • IMP: 是一个函数的具体实现,是一个函数指针。这个函数指针指向的函数。至少有两个参数:
      • 第一个参数:id self, 接收消息的对象(receiver)
      • 第二个参数:SEL _cmd, 方法名

    3. 静态绑定/动态绑定

    • 静态绑定: 在编译期间就能决定运行所调用的函数。
    • 动态绑定: 在运行期才能确定调用函数。

    在OC中, 对象发送消息,就会使用动态绑定机制来决定需要调用的方法。当对象收到消息后,究竟调用哪个方法完全决定于运行期,甚至也可以直接在运行时改变方法,这些特性都使OC成为了一门动态语言。

    4. 关于Swift

    Objective-C有运行时机制,具备动态性,但是Swift没有。Swift是继承了Objective-C有的runtime机制才有了动态性。尝试用Swift来处理消息转发。

    • 动态方法解析
        // 动态方法解析
        override class func resolveInstanceMethod(_ sel: Selector!) -> Bool {
            guard let method = class_getInstanceMethod(self, #selector(runIMP))  else {
                return super.resolveInstanceMethod(sel)
            }
            return class_addMethod(self, Selector(("run")), method_getImplementation(method), method_getTypeEncoding(method))
        }
        @objc func runIMP() {
            print("runIMP")
        }
    
    • 快速向前转发模式
        // 快速消息转发
        override func forwardingTarget(for aSelector: Selector!) -> Any? {
            return Dog()
        }
    
    class Dog : NSObject {
        @objc func eat() {
            print("eat")
        }
    }
    
    • 正常向前转发模式
      在Swift中去除了methodSignatureForSelector:forwardInvocation:这两个方法,在Swift中只有动态方法解析和快速消息转发去实现了。
    • 错误处理
        override class func doesNotRecognizeSelector(_ aSelector: Selector!) {
            
        }
    

    相关文章

      网友评论

        本文标题:iOS 消息转发机制(依据实例展开理论)

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