iOS runtime--消息转发

消息转发概述

Objective-C是一门动态语言,怎么理解动态这一词呢?简单的说就是编译器在编译期可以只知道一个方法的名字,而不需要知道这个方法的实现,只有在运行期间调用该方法的时候,才根据方法名去找到对应方法的实现,这个过程相当于动态绑定一个方法的实现,这就是“动态”。

与“动态”相对的是“静态”,C语言就是一门静态语言,在编译期不仅知道运行时所要调用的函数的名字,而且直接生成了调用函数的指令,将函数地址硬编码在这些指令中。这就是为什么OC的方法没有实现只有声明编译不报错,而C的方法却报错的原因。

正因为是动态绑定,所以在编译期没有报错的程序,在运行时由于根据方法名找不到对应的方法实现,会导致程序的Crash,在Crash之前程序会依次调用几个其他的方法,这就引出了消息转发。

消息转发过程

消息转发是Objective-C语言的特点,当一个对象在运行时接收到无法解读的消息时,就会触发“消息转发”。

在编译期向类发送无法解读的消息是不会报错的,因为在运行期可以继续向类添加方法,所以在编译期,编译器无法知道到底有没有某个方法的实现。
可能听起来有点乱,又是向对象发送无法解读的消息,又是向类发送无法解读的消息。前者可理解为只在运行期有的行为(因为有对象,那肯定是调用了生成对象方法的实现才可能存在,而只有运行期才去调用方法的实现),后者看下面这句代码就知道了

[self performSelector:@selector(humenName)];

方法“humenName”我并没有声明也没有实现,但是这句代码不会报错(会报警告),编译也可以通过,这就是所谓的编译期向类发送无法解读的消息。

消息转发的过程是有一定的规则和步骤的。下面我们看看详细的流程。

1.先看一个runtime库的方法 class_addMethod

class_addMethod的用处是在程序运行时,给一个类添加方法实现的API,其完整API如下:

/** 
 * 根据指定的名字和方法实现给一个类添加方法.
 * 
 * @param cls 被添加方法的类.
 * @param name 指定要添加的方法名称的选择器。
 * @param imp 新的方法实现,这个方法必须至少带有两个参数:self和_cmd
 * @param types 上面那个新方法的参数的类型编码. 
 * 
 * @return 当方法添加成功返回YES , 否则返回NO 
 *  (例如,在类中已经有一个该名字的方法实现,会返回NO)
 *
 * @note class_addMethod 可能会覆盖超类实现, 如果超类也实现了该方法的话,
 * 但是不会替换在本类中已经存在的方法实现,
 * 如果改变本类存在的方法实现,请使用method_setImplementation.
 */
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) 

这个API在消息转发中用到。消息转发分为三个阶段,即“动态方法解析”、“快速消息转发”和“完整消息转发机制”。

2. 动态方法解析

这里结合一个实例来说明,可能会更加容易懂些,新建一个项目,创建一个类HumenModel(继承自NSObject),然后在ViewController中添加如下代码

HumenModel *model = [HumenModel new];
[model performSelector:@selector(humenName)];

因为在HumenModel类中并没有方法humenName的声明和实现,所以,对象model会接受到一个无法解析的消息,此时就会进入消息转发的第一阶段,征询接收者所属的类,看是否能动态添加方法,以处理这个未知的“选择器”,此时就调用该类的类方法

+(BOOL)resolveInstanceMethod:(SEL)sel

当然,如果是类接收到一个无法解析的消息,消息转发第一阶段调用的是类的另一个方法:

+(BOOL)resolveClassMethod:(SEL)sel

方法的参数 sel 就是那个未知“选择器”,返回值是BOOL类型,表示是否新增一个方法来处理未知“选择器”。 现在我们就可以通过class_addMethod方法给类添加一个方法来处理未知“选择器”,代码如下:

+(BOOL)resolveInstanceMethod:(SEL)sel{
    
    // 获取选择器的方法名字
    NSString *selString = NSStringFromSelector(sel);
    if ([selString isEqualToString:@"humenName"]) {
        
        // 给接收者self 添加一个方法sayHello,选择器sel指向方法的实现,方法的类型编码是v@:
        class_addMethod(self, sel, (IMP)sayHello, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
// 一个c函数
void sayHello(id self, SEL _cmd){
    NSLog(@"hello");
}

解释下class_addMethod方法的第四个参数“v@:”
这是sayHello方法的OC类型编码,‘v’表示返回值为void类型,‘@’表示第一个参数是对象,‘:’表示第二个参数是SEL类型的值,其中'@'和':'是固定的,因为每个方法都会有这两个参数。对于其他情况可参照类型编码这篇博客https://blog.csdn.net/ssirreplaceable/article/details/53376915

注意,所添加的方法必须是纯C函数实现的,因为OC的方法名规则和C函数名规则差别是很大的。另外,在运行时,方法resolveInstanceMethod中的代码会被动态插在类里面.

编译运行,打印结果如下:

2019-02-20 17:31:53.411603+0800 MessageTrans[400:8535618] hello

跟预期的结果一样,这样就完成了动态方法解析,无法解析的消息在这一步得到了处理。如果在这阶段没有针对未知“选择器”的做出处理,那么就会进入消息转发的第二阶段。

3.快速消息转发机制

在这一阶段,接收者将要甩锅,看有没有别的接收者可以处理这个无法解析的消息(记得将第一阶段的代码注释掉)。这个一过程会在接收者所在类的下面这个方法中完成

-(id)forwardingTargetForSelector:(SEL)aSelector

参数aSelector是未知选择器,返回值是id类型的值,所以这一阶段只是针对对象来处理,不考虑类方法。

新建一个AnimalModel类,在其实现文件中实现方法humenName,如下:

-(void)humenName{
    NSLog(@"%s",__FILE__);
}

然后在HumenModel.m中实现forwardingTargetForSelector:方法,如下:

-(id)forwardingTargetForSelector:(SEL)aSelector{
    NSString *aSelectorString = NSStringFromSelector(aSelector);
    if ([aSelectorString isEqualToString:@"humenName"]) {
        return [AnimalModel new];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

这样消息交由AnimalModel类处理,运行一下,打印结果如下:

2019-02-20 17:36:49.000246+0800 MessageTrans[2513:8550047] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m

结果正确,我们在第二阶段成功的将消息转发给其他接收者来处理。

对于AnimalModel类,不需要在其头文件中声明humenName方法,没有影响。

如果没有其他接收者,那就会进入“完整消息转发机制”阶段。

4.完整消息转发机制

在这个阶段要处理未知消息,代价就会大些,其实也是类似于快速消息转发阶段,目的都是指定一个接受消息的对象,只不过这里必须覆盖两个方法,即methodSignatureForSelector:和forwardInvocation:。

methodSignatureForSelector:的作用在于为另一个类实现的消息创建一个有效的方法签名,必须实现,并且返回不为空的methodSignature,否则会crash。
forwardInvocation:的作用是绑定消息接收者。

看代码实现:

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
    if ([super methodSignatureForSelector:aSelector] == nil) {
        // 手动创建
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
    
    // 自动创建方法签名
//    AnimalModel *animalModel = [AnimalModel new];
//    return [animalModel  methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    AnimalModel *model = [AnimalModel new];
    
    if ([model respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:model];
    }else{
        [self doesNotRecognizeSelector:anInvocation.selector];
    }
}

解释下上面的代码:方法签名有两种方式,一个是手动创建签名,一个是自动创建签名,看代码可以明白,着重要讲的是下面这句

NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];

参数值"v@:"为类型编码,前面有提到,这里就不说规则了。原则上来说,该编码的类型应该与未知方法的参数对应,所以用根据未知方法自动创建方法签名更好。但是如果非要用手动创建方法签名的话,在写方法signatureWithObjCTypes:的参数值时要注意两点

  • 方法的返回类型必须有,不能省略,比如这里是返回空类型,所以对应第一个编码为v
  • 方法的默认参数self和_cmd对应的类型编码不能写错,固定为"@:"

满足上面两点签名就会有效,否则会导致crash,至于方法签名中参数个数与未知方法不对应是没有问题的,比如说类型编码为"v@:@@@",而未知方法为:humenName(没有参数),是不会导致crash,只不过这里的参数写多少个,会影响方法forwardInvocation:的参数值anInvocation的变化。

在第二个方法forwaidInvocation:必须判断接收者是否能响应未知消息,否则直接执行[anInvocation invokeWithTarget:model]在接收者无法响应位置消息时会导致崩溃。如果指定的接收者不能响应未知选择器,那么没办法了只能抛出异常,执行doesNotRecognizeSelector:方法,程序崩溃。

这里在讲下刚才提到的,方法签名的参数个数的问题,看下面的代码:

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
    if ([super methodSignatureForSelector:aSelector] == nil) {
        // 手动创建
        NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@@:@@"];
        return signature;
    }
    return [super methodSignatureForSelector:aSelector];
    
    // 自动创建方法签名
//    AnimalModel *animalModel = [AnimalModel new];
//    return [animalModel  methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    AnimalModel *model = [AnimalModel new];
    
    SEL sel = anInvocation.selector;
    NSMethodSignature *sign = anInvocation.methodSignature;
    NSLog(@"%lu--%@",(unsigned long)sign.numberOfArguments,NSStringFromSelector(sel));
    
    if ([model respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:model];
    }else{
        [self doesNotRecognizeSelector:anInvocation.selector];
    }
}

AnimalModel类中humenName方法实现如下:

-(void)humenName{
    NSLog(@"%s",__FILE__);
}

运行打印结果如下

2019-02-21 15:31:41.851617+0800 MessageTrans[14393:9247960] 4--humenName
2019-02-21 15:31:48.716371+0800 MessageTrans[14393:9247960] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m

根据打印结果可以看到,方法签名中参数个数,与anInvocation中的参数个数值是对应的,并不与未知选择器humenName的参数个数对应(参数个数为0),而且这种不对应并不会影响AnimalModel类中方法humenName的正常执行。

拓展:如果对象调用一个未指定参数值的未知消息,但是在另一个类中有该方法的实现,会怎样呢
刚才我们调用的未知方法是没有参数的,我们实现下有参数不指定值得代码
viewController.m -> viewDidLoad

HumenModel *model = [HumenModel new];
[model performSelector:@selector(humenName:)];

AnimalModel.m

-(void)humenName:(NSInteger)number{
   NSLog(@"%s - %ld",__FILE__,(long)number);
}

HumenModel.m中的代码和上面的相同,编译运行,发现崩溃了,原因容易想到是这个消息没有参数值,当在AnimalModel中调用humenName:时,就会报野指针。这个问题怎么解决呢,见下面的代码

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
   
   // 自动创建方法签名
   AnimalModel *animalModel = [AnimalModel new];
   return [animalModel  methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
   AnimalModel *model = [AnimalModel new];
// 给anInvocation设置参数值
   NSInteger number = 10;
   [anInvocation setArgument:&number atIndex:2];// 为什么是2,因为0是self参数,1是_cmd参数,返回值类型不属于参数。
   
   if ([model respondsToSelector:anInvocation.selector]) {
       [anInvocation invokeWithTarget:model];
   }else{
       [self doesNotRecognizeSelector:anInvocation.selector];
   }
}

这样再运行看结果如下:

2019-02-21 16:13:56.024182+0800 MessageTrans[15139:9320442] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m - 10

运行正常

到这一步消息转发的整个流程就讲完了。

runtime底层代码

消息转发的实际应用

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

推荐阅读更多精彩内容

  •   最近看了『神奇的 BlocksKit』系列,里面说到动态代理是BlocksKit的精华部分,对于使用block...
    foreverSun_122阅读 1,102评论 1 7
  • 当我们像一个对象发送消息[Receiver message],Receiver没有实现该消息,即[Receiver...
    AlvinCrash阅读 798评论 1 5
  • 相信大家对消息转发机制都不陌生,或许没有使用过,但耳濡目染也听了不少object-c消息转发,动态解析等神奇的功能...
    coco_CC阅读 194评论 0 1
  • 以前知道苹果执行方法是通过消息执行的,当对应的对象或者类无法处理该消息时,苹果就会启动消息转发机制,通过这一机制,...
    海浪萌物阅读 397评论 0 0
  • 消息转发机制 假设说我们声明一个类, 初始化对象, 并且在此类声明一个方法, 调用方法的时候底层是怎么处理的呢? ...
    软件iOS开发阅读 243评论 0 0