Objective-C消息的传递和转发

titleImg.png

很久没有写简书了,今天得空更新一篇。其实有关于Objective-C消息的传递和转发,简书上已经有很多类似的文章了。但是,为了让自己记忆深刻,以便自己后续查找,所以还是记录下来。

一. 消息的传递

使用某个对象去调用成员方法,这个操作在我们实际开发中在常见不惯了,当时往往我们并没去实际研究,其底层的实际操作是怎样的。可能有的小伙伴会说,我知道怎么调用就可以了,干嘛还要去研究底层。这个的话,纯属个人兴趣吧。喜欢知其所以然,对我们的自身的提高也是一件非常好的事情。

某个对象调用其所属类的成员方法,这个在Objective-C(以下简称OC)中的术语叫“消息传递(pass a message)”。我们的成员方法有生命格式是,返回值、方法名、参数、方法实体。这些知识我们的语法名称,编译器内部,他们又有不同的叫法。每个成员方法,又叫做“消息”。“消息”主要有以下几个东西构成:“消息名称”或“选择子(selector)”、“参数”、“返回值”。
由于OC是C的超集,所以我们在弄清楚OC的方法传递之前,我们先来回顾下C语言的函数调用。C语言使用的是“静态绑定”(static binding),也就是说,程序在编译阶段就已经决定了各个步骤所需调用的函数名以及其具体实现代码。举例说明:

# import <stdio.h>

void printHello() {
    printf("Hello, world!\n");
}

void printGoodbye() {
    printf("Goodbye, world!\n");
}

void gratting(int type) {
      if (type == 0) {
          printHello();
      } else {
          printGoodbye();
      }
}

此时如果我们在不考虑内联函数的情况下,那么编译器在编译代码的时候就已经知道了程序中有printHello和printGoodbye两个函数了,于是编译器会直接把此两个函数编译成相关指令,且两个函数的存放地址也已经硬编码在指令之中了。那么,如果我们把上面的代码稍加修改,会怎么样呢?修改如下:

# import <stdio.h>

void printHello() {
    printf("Hello, world!\n");
}

void printGoodbye() {
    printf("Goodbye, world!\n");
}

void gratting(int type) {
      void *(fnc)();
      if (type == 0) {
          fnc = printHello;
      } else {
          fnc = printGoodbye;
      }
      fnc();
}

此时,我们使用了“动态绑定”(dynamic bingding)。以为在所要调用的函数直到执行了if else判读之后,才能知道具体要调用那个方法。编译器在这种情况下生成的指令与刚才的例子则有所不同。在事先的例子中,在if else判断语句中都有函数调用指令。但在刚才的例子中,只有一个函数调用指令,不过待调用的函数的地址无法硬编码到指令之中,而是在执行到了if else 语句后才知道具体的调用函数。那么这就是我们要讲的第一个概念“动态绑定”(dynamic bingding)。

在OC中,某实例对象调用某个成员方法,我们称其为向该实例对象传递某条消息。此时将会使用到我们上述的动态绑定机制来决定需要调用的方法是哪一个。在OC的底层,所有的成员方法都是C语言函数,当实例对象调用某个方法的时候,他到底该去调用哪个底层的C函数?这就得在程序的运行期才能知道,甚至,程序在运行期,知道了该调用那一个底层函数后,我们还可以动态的去改变它。由于OC具备这样的相关功能,也使其成了一种真正的动态开发语言。

在某对象调用某成员方法时候,我们通常会这样写:

id return value = [someObject methodName: parameter]; 

在上述代码中,someObject 叫做消息的“接受者”,messageName叫做“选择子”。选择子和参数合起来就称之为一条“消息”。当编译器看到了此条消息后,编译器就会将其转换一条标准的C语言函数调用,所调用的函数就是消息传递机制中的和兴函数,objc_msgSend。该方法原型如下:

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

objc_msgSend函数会要依据接收者与选择子的类型来调用适当的方法。为了使接收者能准确的调用到成员方法,objc_msgSend会在接受者所属类中去遍历查找“方法列表”,如果能找到与选择子名称相符的方法,就直接跳转到其实现代码。如果找不到,就会沿着接受者所属类的继承体系继续向上查找,等找到了合适的方法之后再进行跳转。如果最终还是未能找到与选择子名称相符的方法,那就执行“消息转发(message forwarding)”操作。

综上所述,某类的实例对象在调用其成员方法的时候似乎需要执行很多部的操作。可能我们会提出疑问,如果该过程会执行这么多步骤,那么在资源消耗上的开销会不会很大呢?其实我们不用担心这个问题,编译器在执行选择子匹配的过程中,会将匹配的结果缓存到“快速映射表(fast map)”里面,切每个类都有这样一块缓存区。有了缓存区过户,方法调用的匹配过程就会变得非常的快速。当然,话说回来,即使有这种所谓的快速缓存区,与之“静态绑定的函数调用操作”相比较,它还是慢很多,但OC是一门动态语言,这里且暂且不表。

上述的介绍,知识描述了部分消息调用时的底层原理,在实际开发中,我们还可能会遇到其他一些“边界情况”,若如此情况,则需要交由OC运行环境中的另一些函数来处理。我们这里只做简单介绍,不具体阐述。“边界情况”如下:

  • objc_msgSend_stret: 如果待发送的消息的返回值是一个“结构体(struct)”,那么可交由此函数来处理。只有当CPU的寄存器能够容纳的下消息返回数据类型时,这个函数才会处理此消息。相反,则需由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息返回的结构体。
  • objc_msgSend_fpret: 如果待发送消息的返回值是浮点数,那么可交由此函数来处理。在某些架构的CPU中调用函数时,需要对“浮点数寄存器”做特殊处理,也就是说,通常所用的objc_msgSend在这种情况下并不合适。这个函数是为了处理x86等架构CPU中某些令人稍觉奇怪的状况。
  • objc_msgSendSuper:如果需要给某实例对象所属类的父类发送消息, 例如我们常用到的
[super message: parameter];

此时就可交由此函数来处理。
前面我们说到了,objc_msgSend等消息传递函数一旦找到了与之匹配的方法实现就会实现跳转,编译器之所以会这么做,就是因为OC中所有类的成员方法在底层都是标准的C语言函数,其原型如下:

<return_type> Class_selector(id self,  SEL,  _cmd,  ...);

真正的函数名可能和上述有所差别,这里我用“类(class)”和“选择子(selector)”来命名是想其工作原理。OC中每一个类都有一张表,表中的方法指针“methodPointer”都会只指向这样的函数,而选择子的名称则是方法列表中的“key”。objc_msgSend等函数正是通过这张表来寻找应该执行的方法,并跳转至实现代码中。

“消息传递”总结:

  • 某实例对象调用其成员方法,称之为给该对象传递一条消息。
  • 消息有接受者,选择子以及参数列表构成。给某实例对象“发送消息(invoke a message)”就是在改对象上调用方法“call a method”。
  • 发给某对象的全部消息都要由“动态消息派发系统(dynamic message dispatch system)”来处理,该系统会查出对应的方法,并执行其代码。

二. 消息的转发

上文我们了解了OC消息的传递机制,接下来我们来说说消息的转发机制。
很多时候,某实例对象在收到了无法解读的消息之后,该怎么办呢?
在开发过程中,我在某个类中申明了某个方法,但是我们不具体实现,此时我们编译我们的程序,我们会发现,编译器并不会报错。因为OC门动态语言,我们可以在运行期,动态的向该类中添加我们刚刚申明的方法的具体实现代码。但是此时,如果我们并没有动态添加该方法的具体实现,也就是说,我们用某实例去调用该方法,但是该方法不能被正常调用,因为我们有去实现它的具体逻辑,此时就会启动“消息转发机制”。

在我们的开发过程中,你可能遇见过经由消息转发流程处理的消息,只是我们未加留意它。如果在控制台看到以下报错提示,那就说明我们向某一实例发送了一条其无法解读的消息,从而启动了消息转发机制,并将此消息转发给了NSObject的默认实现。控制打印如下:

- [__NSCFNumber lowercaseString]: unrecognized selector send to instance 0x87
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '- [__NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87'

上面控制台答应的消息是由NSObject的“doesNotRecognizeSelector:”方法所抛出的异常,此异常表明:消息的接受者的类型是 __NSCFNumber, 而该接收者无法解读名为“lowercacseString”的选择子。在本例中,消息转发过程以程序crash而告终,不过,开发者在编写自己的类的时候,可在转发过程中设置挂钩,用以执行预定的逻辑,而让程序不会crash掉。

好了,看完实例,我们在具体说说消息转发的具体步骤。消息转发可以分两个阶段。第一个阶段先征询接收者所属的类,看其能否动态添加方法,以处理当前这个未知选择子。这个步骤叫做“动态方法解析(dynamic method resolution)”。第二阶段则涉及到完整的消息转发机制。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这里又要分为两个小步骤,首先,接收者会查看,看有没有其他对象也就是“备用接收者(replacement receiver)”能处理该未知消息,如果有,则运行期系统会将此未知消息转发给那个可执行此消息的对象,于是消息转发结束。如果接收者未能查看到备用接收者,则启动完整消息转发机制,运行期系统会把与此未知消息有关的所有信息和细节都封装到NSInvocation的实例对象中,然后再给接收者一次机会,令其设法解决当前未处理的这条消息。

2.1 动态方法解析

上述过程就是整个消息转发过程,但是里面还有些知识点,这给大家具体解释下。首先我们看看上文中提及到的动态解析方法。在消息转发的第一个阶段,当实例对象收到无法解读的消息时,首先会去征询接收者所属类,看其能否动态添加方法来处理该未知消息。此过程叫做“动态方法解析”。
在对象收到无法解读消息后,首先会调用所属类的下列方法:

+ (BOOL) resolveInstancemethod: (SEL) selector;

该方法的参数,就是那个未知的选择子。其返回值为布尔类型,表示这个类是否能新增一个实例方法来处理该未知消息。还与这么一种情况,当我们通过类直接调用其类方法的时候,切该类方法也是类当前不能响应的方法,此时同样,会去征询类本身,看其能否动态新增一个类方法来处理该未知消息。此时调用的方法和上面类似"resolveClassMethod:"。

在使用这个方法的时候,有一个前提,动态添加的方法的具体实现,我们已经提前编码好了。只等在运行期动态插入即可。这里我们举个例子来说明, 当然,文章最后我们会用一个完整的简单例子来说明消息的传递和转发机制。这里,我们暂且先说这个动态插入。在开发的过程中,我们很多时候用@dynamic来修饰类的属性,例如我们在访问CoreData框架中的“NSManagerObjects”对象的属性时,我们就可以用到动态插入,因为实现这些属性的所需的setter和getter是在编译器就能确定的。好了,我们来看看如果用“resolveInstancemethod”方法来实现@dynamic属性的:

id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);

+ (BOOL)resolveInstanceMethod:(SEL)selector {
    NSString *selectorString = NSStringFromSelector(selector);
    if (/* selector is from a @dynamic property */) {
        if ([selectorString hasPrefix:@"set"]) {   
            class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
        } else {
            class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
    }
    return [super resolveInstanceMethod: selector];
}

此段代码首先是将选择子转为字符串,然后检测其是否表示设置方法。如果前缀位"set",则表示为设置方法,否则就是获取方法。不管是哪种情况,都会把处理改选择子的方法加到类里面。所添加的方法是用C函数实现的。C函数可能会用代码来操作相关的数据结构,类之中的属性数据就就存放在那些数据结构里面。以coreData为例,这些存取方法也许要和后端数据库通信,以便获取或更新相应的值。

2.2 备用接收者

在消息转发的第二阶段,接收者拥有第二次机会来处理未知消息,在这一步骤中,运行期会判断接收者能不能将此未知消息转发给其他接收者来处理。这个其他接收者就是备用接收者。此时会调用下面这个方法:

- (id) forwardingTargetForSelector: (SEL) selector;

该方法参数就是将要转发的选择子。如果说接收者能找到能处理该未知选择子的其他对象,则将其返回,若找不到,就返回nil。

2.3 完整的消息转发

如果说消息转发算法已经来到这一步的话,那么唯一能做的就是启动完整消息转发机制。首先,创建一个NSInvocation对象,此对象包含有选择子,目标以及参数。然后将尚未处理的选择子,及其先关全部细节都封装于此对象中。在触发NSInvocation对象时,“消息派发系统(message-dispatch-system)”将亲自出马,把消息派发给目标对象。其方法如下:

-(void)forwardInvocation: (NSInvocation *)invocation;

消息转发全流程:


屏幕快照 2019-04-19 上午10.35.11.png

为了说明消息转发机制的意义,下面示范如何以动态方法解析来实现 @dynamic 属性。假设要编写一个类似于“字典”的对象,他里面可以容纳其他对象,只不过开发者要直接通过属性来存取其中的数据。这个类的设计思路是:由开发者来添加属性定义,并将其声明为 @dynamic ,而类则会处理自动处相关属性的存取。
具体代码如下:

.h文件内容
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SxsAutoDictionary : NSObject
@property(nonatomic, copy) NSString *string;
@property(nonatomic, strong) NSNumber *number;
@property(nonatomic, copy) NSDate *date;
@property(nonatomic, strong) id opaqueObject;
@end
NS_ASSUME_NONNULL_END
//其实这些属性都没有任何的实际意义,所以属性的类型和属性名称你可以随意设置。

.m文件内容
#import <objc/runtime.h>
#import "SxsAutoDictionary.h"
@interface SxsAutoDictionary ()
@property(nonatomic, strong) NSMutableDictionary *backingStore;
@end
@implementation SxsAutoDictionary
@dynamic string, number, date, opaqueObject;
- (id)init {
    if (self = [super init]) {
        _backingStore = [NSMutableDictionary dictionary];
    }
    return self;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selStrin = NSStringFromSelector(sel);
    if ([selStrin hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
    } else {
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    return YES;
}
id autoDictionaryGetter(id self, SEL _cmd) {
    //Get the backing store from the objcet
    SxsAutoDictionary *typeSelf = (EOCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typeSelf.backingStore;
    
    //The key is simply the selector name
    NSString *key = NSStringFromSelector(_cmd);
    //Return the value
    return [backingStore objectForKey:key];
}
void autoDictionarySetter(id self, SEL _cmd, id value) {
    //Get the backing store from the objcet
    SxsAutoDictionary *typeSelf = (EOCAutoDictionary *)self;
    NSMutableDictionary *backingStroe = typeSelf.backingStore;
    /**
     *  The selector will be for example , "setOpaqueObject".
     *  We need to remove the "set", ":", and lowercase the first letter of the remainder
     */
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    // Remove the ":" at the end
    [key deleteCharactersInRange: NSMakeRange(key.length - 1, 1)];
    // Remove the 'set' prefix
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    // lowercase the first character
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    if (value) {
        [backingStroe setObject:value forKey:key];
    } else {
        [backingStroe removeObjectForKey:key];
    }
}
@end

该类的实际调用:
SxsAutoDictionary *dic = [[SxsAutoDictionary alloc] init];
dic.string = @"Set a String";
NSLog(@"Print: %@", dic.string);

控制台输出结果:
2019-04-19 11:21:48.503465+0800 EffectiveObjective-C_TestApp[9036:2325913] Print: Set a String

消息转发总结:

  • 如果对象无法响应某个选择子,则进入消息转发流程。
  • 通过运行期的动态方法解析,我们可以在需要用到某个方法时再讲其动态插入到类中。
  • 对象可以把无法响应的选择子转发给其他备用对象来处理。
  • 经过以上两个步骤知乎,如果还是没法处理未知消息,则启用完整消息转发机制。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容