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 <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!) {
        
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容