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

先总结,后解释

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 

@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 =  _cmd = eatTogetherWith: value = 

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

说明:

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

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

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

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

#import 
#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 消息转发机制(依据实例展开理论))