【iOS_Development】消息转发机制简述

前言

在Objective-C中,如果某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法;在底层,所有方法都是普通的C语言函数,然而对象接收消息之后,究竟该调用哪个方法则完全取决于运行期决定,设置可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。

发消息

给对象发送消息可以这样来写

id returnValue = [someObject messageName:param];

其原型如下

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

故编译器会将其转换为如下函数

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

objc_msgSend函数会依据接收者(returnValue)与选择子(messageName)的类型来调用适当的方法;为了完成此操作,该方法需要在接收者所属的类中搜寻其方法列表,如果能找到与选择子名称相符的方法,就跳至其实现代码;若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转;若最终还是找不到相符的方法,那就执行消息转发操作。

消息调用过程还会存在如下边界情况
  a. objc_msgSend_stret:如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息,否则,交由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体
  b. objc_msgSend_fpret:如果消息返回的是浮点数,那么可交由此函数处理,在某些架构的CPU中调用函数时,需要对“浮点寄存器”做特殊处理,也就是说,通常所用的objc_msgSend在这种情况下并不合适。这个函数是为了处理x86等架构CPU中某些令人稍觉惊讶的奇怪情况
  c. objc_msgSendSuper:如果给超类发消息,例如[super message:parameter],那么就交由此函数处理。也有另外两个与objc_msgSend_stret和objc_msgSend_fpret等效的函数,用于处理发给super的相应消息


消息转发三部曲

若想令类能理解某条消息,我们必须以程序码实现出对应的方法才行;但是,在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现(即编译器无法确定某类型对象到底能解读多少种选择子,因为运行期还可以向其中动态新增)。当对象接收到无法解读的消息后,就会启动“消息转发”机制,程序员可经由此过程告诉对象应如何处理未知消息

  • 消息转发分为两大阶段
      第一阶段先征询接收者,所属的类,看其能否动态添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析

      第二阶段涉及“完整的消息转发机制”,如果运行期系统已经把第一阶段执行完了,那么接收者就无法再以动态新增方法的手段来相应包含该选择子的消息了。此时运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步:
        1). 首先请接收者看看有没有其他对象能处理这条消息,若有,则运行期系统会把这条消息转给那个对象,于是消息转发过程结束,一切如常
        2). 若没有“备援接收者” ,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息

第一步:动态方法解析

对象接收到无法解读的消息后,首先将调用其所属类的以下方法(如果类无法立即响应某个选择子,那么就会启动消息转发流程)

// 未知选择子为实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel

// 未知选择子为类方法
+ (BOOL)resolveClassMethod:(SEL)sel
  • 使用这种方法的前提是:相关方法的实现代码已经写好,只等着运行时候动态插在类里面就可以了
  • 运行期添加方法时使用此函数

使用示例如下

.h
@property (nonatomic, strong) NSString *strTest;

.m
@dynamic strTest;

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    NSLog(@"selectorString ==> %@", selectorString);
    
    if ([selectorString hasPrefix:@"set"]) {
        // 所添加的方法使用纯C函数实现的
        // (IMP)autoautoDictionarySetter:函数指针,指向待添加的方法
        // "v@:@“:待添加方法的类型编码,本例中,开头字符表示方法的返回值类型,后续字符表示其所接受的各个参数
        // class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method))
        class_addMethod(self, sel, (IMP)autoautoDictionarySetter, "v@:@");  
    } else {
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    return YES;
}

id autoDictionaryGetter(id self, SEL _cmd) {
    return @"getter";
}

void autoautoDictionarySetter(id self, SEL _cmd, id value) {
    NSLog(@"Setter");
}


第二步:备援接收者

假使动态方法解析中未能够处理未知选择子,接下来运行期系统会问接收者能不能把这条消息转给其他接收者来处理,即调用以下方法

// 返回可以处理未知选择子的对象(当返回非self\非nil时,消息被转给新对象执行)
- (id)forwardingTargetForSelector:(SEL)aSelector
  • 若想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做
  • 转发给另一个对象、改变方法时使用此函数

第三步:完整的消息转发

如果转发算法来到完整的消息转发这一步的话,那么唯一能做的就是启用完整的消息转发机制了;首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中;此对象包含选择子、目标及参数,再触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象,此时会调用以下函数

- (void)forwardInvocation:(NSInvocation *)anInvocation

这个方法实现的很简单:只需修改调用目标,使消息在新目标上得以调用即可;然而这样实现出来的方法与“备援接收者”方案实现的方法等效,所以很少有人采用这么简单的实现方式;比较有用的实现方式为:在触发消息之前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子等等


  实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法;这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject;最后调用了NSObject类的方法,那么该方法还会继续调用“doesNotRecongizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理

  • 需要转发给多个对象时使用此函数

为了更好的去理解整个过程可以参看以下此流程图



总结

接收者在每一步中均有机会处理消息,步骤越往后,消息处理的代价就越大,最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来,如果这个类的实例稍后还收到同名选择子,那么根本就无须启动消息转发流程。若想在第三步里将消息转给备援接收者,那还不如把转发操作提前到第二步,因为第三步只是修改了调用目标,这项改动放到第二步执行更为简单,不然的话,还得创建完整的NSInvocation


声明:本文参考自《Effective Objective C 2.0:编写高质量iOS与OS X代码的52个有效方法》


GitHub主页

CSDN Blog

Email:jinjob@icloud.com

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,830评论 18 139
  • 目录 Objective-C Runtime到底是什么 Objective-C的元素认知 Runtime详解 应用...
    Ryan___阅读 1,950评论 1 3
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,757评论 0 9
  • 前言 Objective-C是一门非常动态的语言,以至于确定调用哪个方法被推迟到了运行时,而非编译时。与之相反,C...
    VV木公子阅读 6,209评论 3 31
  • 6.理解“属性”这一概念 @dynamic关键字:告诉编译器不要自动创建实现属性所用的实例变量,也不要为其创建存取...
    Code_Ninja阅读 733评论 0 3