Runtime — 消息转发

前言

如果在动态解析阶段不做任何处理的话,我们调用一个未实现的方法会crash,下面来分析一下,crash之前系统还做了什么?

一、探索消息转发

1. instrumentObjcMessageSends打开log开关

#import <Foundation/Foundation.h>
#import "DZStudent.h"

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        DZStudent *student = [DZStudent alloc] ;
        instrumentObjcMessageSends(true);
        [student saySomething];
        instrumentObjcMessageSends(false);

    }
    return 0;
}

extern void instrumentObjcMessageSends(BOOL flag):是苹果的私有API,我们可以控制log开关,打印日志信息。

日志文件位置:/tmp/msgSend-xxx

日志文件位置

2. 查看日志文件

我们可以发现,方法在调用报失败doesNotRecognizeSelector之前的调用顺序:resolveInstanceMethod -> forwardingTargetForSelector -> methodSignatureForSelector -> doesNotRecognizeSelector

resolveInstanceMethod是动态方法决议,我们上一文已经做了分析,本文只针对后边的方法进行源码分析。

二、快速转发

1. forwardingTargetForSelector分析

我们全局搜索后,发现这个方法是NSObject中实现的方法,只做了返回nil的操作:

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

我们可以结合方法介绍或者官方文档来进行分析:

2. 方法说明(看discussion):

  1. 该方法的目的就是不能处理方法的时候,交给另外一个对象来执行,但是不能返回self,否则会一直找不到陷入死循环。
  2. 该方法效率很高,如果不实现或者返回nil,会走到相对效率低的forwardInvocation: 方法进行处理。
  3. 所以我们称forwardingTargetForSelector快速转发forwardInvocation慢速转发
  4. 被转发的消息接收者,参数和返回值等需要和原方法相同。

3. 方法使用

当访问DZStudent未实现的saySomething方法时,可以使用- (id)forwardingTargetForSelector:(SEL)aSelector进行方法转发,用DZTeacher这个实现saySomething方法的对象来接收,具体实现代码如下:

main.m

DZStudent *student = [DZStudent alloc];
instrumentObjcMessageSends(true);
[student saySomething];
instrumentObjcMessageSends(false);

DZStudent.m

// 消息转发流程
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(saySomething)) {
        return [DZTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

DZTeacher.m

@implementation DZTeacher
- (void)saySomething{
    NSLog(@"%s",__func__);
}
@end

打印结果

// 失败打印
2020-06-17 15:58:28.646428+0800 008-方法查找-消息转发[17172:5671635] -[DZStudent saySomething]: unrecognized selector sent to instance 0x101805980
2020-06-17 15:58:28.658327+0800 008-方法查找-消息转发[17172:5671635] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[DZStudent saySomething]: unrecognized selector sent to instance 0x101805980'

// 成功打印
2020-06-17 14:58:14.506074+0800 008-方法查找-消息转发[10077:5556271] -[DZStudent forwardingTargetForSelector:] -- saySomething
2020-06-17 14:58:14.507704+0800 008-方法查找-消息转发[10077:5556271] -[DZTeacher saySomething]

通俗点讲,这个方法的作用就是,自己的活自己干不了,就交给能干活的人去干。

三、慢速转发

当快速转发流程也没有实现,或者返回nil,就进入慢速转发流程。

1. methodSignatureForSelector

同样在源码中全局搜索之后,我们发现这个方法也是NSObject中实现的方法:

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject instanceMethodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

官方文档:

方法说明:

该方法用于协议的实现,如果有对象未能直接实现的消息,则重写此方法返回适当的方法签名。然后将签名对象作为参数传给forwardInvocation方法,在forwardInvocation里边将消息给能处理该消息的对象,避免最后调用didNotRecognizeSelector方法导致崩溃。

下来我们继续了解 forwardInvocation 方法:

2. forwardInvocation

同样是在源码中全局搜索之后,我们发现这个方法也是NSObject中实现的:

+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

官方文档:

forwardInvocationmethodSignatureForSelector必须是同时重写。并且该方法可以自由指派多个对象接受该消息。

3. doesNotRecognizeSelector

// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

我们可以发现,最终是doesNotRecognizeSelector方法抛出的异常,所以我们可以重写forwardInvocation方法,这样不执行父类的方法,程序就不会崩溃了。

4. 方法使用

forwardInvocation方法中,我们可以把这个方法看成是一个未知方法收集箱,在这里可以随意选择你可以处理的方法,进行归类集中处理。

main.m

#import <Foundation/Foundation.h>
#import "DZStudent.h"

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DZStudent *student = [DZStudent alloc];
        instrumentObjcMessageSends(true);
        [student saySomething];
        instrumentObjcMessageSends(false);
    }
    return 0;
}

DZTeacher.m

@implementation DZTeacher

- (void)saySomething{
    NSLog(@"%s",__func__);
}
@end

DZStudent.m
备注:关于方法签名串"v@:"可以参考官方文档:方法签名Type Encodings

// 返回一个方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomething)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s ",__func__);

   SEL aSelector = [anInvocation selector];
   if ([[DZTeacher alloc] respondsToSelector:aSelector])
       [anInvocation invokeWithTarget:[DZTeacher alloc]];
   else
       [super forwardInvocation:anInvocation];
}
// 打印
2020-06-17 17:17:08.148187+0800 008-方法查找-消息转发[24033:5801203] -[DZStudent methodSignatureForSelector:] -- saySomething
2020-06-17 17:17:08.149424+0800 008-方法查找-消息转发[24033:5801203] -[DZStudent forwardInvocation:]

当然此处转发方法也可以什么都不做处理,也仅仅是转发不出去而已,并不会崩溃。

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s ",__func__);
}
// 打印
2020-06-17 18:24:21.692113+0800 008-方法查找-消息转发[32243:5925309] -[DZStudent methodSignatureForSelector:] -- saySomething
2020-06-17 18:24:21.699464+0800 008-方法查找-消息转发[32243:5925309] -[DZStudent forwardInvocation:]

四、总结

  1. 动态方法决议也没有做处理时,就会进入快速转发(forwardingTargetForSelector)阶段。
  2. 如果快速转发也没有做处理,会继续到慢速转发(forwardInvocation)阶段。
  3. 即使forwardInvocation中不实现后续方法也不会崩溃。
  4. forwardInvocationforwardingTargetForSelector类似,都可以将A类的方法转发的B类的实现中去,但是forwardInvocation优点是更加灵活,forwardingTargetForSelector只能转发发到固定的一个对象。而forwardInvocation可以转发的多个对象中去,甚至不做处理,也仅仅是转发不出去而已,并不会崩溃。

消息的流转及对应的作用如下所示:

流转:消息发送 -> 消息查找 -> 动态方法决议 -> 快速转发 -> 慢速转发
作用:像某个对象或类发送消息 -> 自己有没有处理 -> 自己有没有特殊处理(动态方法决议) -> 指定的人有没有处理 -> 爱谁处理谁处理

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