runtime message forwarding

前言

本篇文章是研究消息转发的机制,苹果的消息转发机制就像一条链,消息传送链越长则消耗也越大,最好是在第一级就可以直接发送消息。

我们必须要先了解objc_msgSend函数调用的检测过程:

  1. 第一步:检测这个selector是不是要忽略的。
  2. 第二步:检测这个target是不是nil对象。nil对象执行任何一个方法不会Crash是因为会被忽略掉。
  3. 第三步:查找这个类的IMP,也就是方法实现。先从方法缓存列表cache中查找,若找到则跳到对应的函数去执行;若找不到,则查找方法分发表。如果分发表找不到就到父类的分发表去找,直到找到或者查找到NSObject根类为止。
  4. 第四步:前三步都找不到,则开始进入动态方法解析了

动态解析

其流程是这样的:

  1. 第一步:+ (BOOL)resolveInstanceMethod:(SEL)sel实现方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过class_addMethod函数动态地添加方法,消息得到处理,此流程完毕。
  2. 第二步:在第一步返回的是NO时,就会进入- (id)forwardingTargetForSelector:(SEL)aSelector方法,这是运行时给我们的第二次机会,用于指定哪个对象响应这个selector。不能指定为self。若返回nil,表示没有响应者,则会进入第三步。若返回某个对象,则会调用该对象的方法。
  3. 第三步:若第二步返回的是nil,则我们首先要通过- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector指定方法签名,若返回nil,则表示不处理。若返回方法签名,则会进入下一步。
  4. 第四步:当第三步返回方法方法签名后,就会调用- (void)forwardInvocation:(NSInvocation *)anInvocation方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等
  5. 第五步:若没有实现- (void)forwardInvocation:(NSInvocation *)anInvocation方法,那么会进入- (void)doesNotRecognizeSelector:(SEL)aSelector方法。若我们没有实现这个方法,那么就会crash,然后提示打不到响应的方法。到此,动态解析的流程就结束了。

验证动态解析

这里提供了三个小例子,验证解析流程。

  1. 第一个例子:提供声明,但是不提供方法实现。验证当找不到方法的实现时,动态添加方法。
  2. 第二个例子:不提供声明,将调用对象修改成其它类实例。验证修改处理消息的对象。
  3. 第三个例子:不提供声明,不修改调用对象,但是修改调用的方法

例子一

我们先声明一个Dog类,提供一个eat方法,但是我们只提供声明,却不实现这个方法。

声明Dog类

@interface Dog : NSObject

// 我们只声明,而不实现
- (void)eat;

@end

实现Dog类

我们不实现实例方法-eat,而是添加了一个C语言的eat方法,注意这个eat方法不是Dog的实例方法。然后我们看一下实现如下:

#import "Dog.h"
#import <objc/runtime.h>


@implementation Dog

// 第一步:实现此方法,在调用对象的某方法找不到时,会先调用此方法,允许
// 我们动态添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
  // 我们这里没有给dog声明有eat方法,因此,我们可以动态添加eat方法
  if ([NSStringFromSelector(sel) isEqualToString:@"eat"]) {
    class_addMethod(self, sel, (IMP)eat, "v@:");
    return YES;
  }
  
  return [super resolveInstanceMethod:sel];
}

// 这个方法是我们动态添加的哦
//
void eat(id self, SEL cmd) {
  NSLog(@"%@ is eating", self);
}

@end

由于我们没有提供Dog实例方法-eat,因此在调用此方法时,runtime会调用+ (BOOL)resolveInstanceMethod:(SEL)sel方法,允许我们动态添加方法。当然我们也可以返回NO。若返回NO,就继续往下传递。

我们这里通过class_addMethod动态地添加方法eat,这个eat方法是C函数。为什么这里的void eat(id self, SEL cmd)有两个参数呢?我们什么时候传有参数?这是不是很奇怪呢?其实是这样的,编译器在将函数转换成objc_msgSend函数调用时,都会自动添加上id self, SEL cmd这两个参数,因此我们就可以拿得到。

测试例一

我们如此调用,由于我们声明了-eat方法,因此是可以调用的。但是我们却没有实现它,编译时是没有问题的,因为objective-c在编译时,只是转换成objc_msgSend函数,而对于实现是在链接时才会去调用。

Dog *dog = [[Dog alloc] init];
[dog eat];

其打印结果如下:

<Dog: 0x7fdf53f08750> is eating

说明它成功的添加了我们的C语言方法的实现作为-eat方法的实现。到此,例一就圆满验证通过了!

例子二

我们声明一个Pig类,但是这个类不声明-eat方法。

声明Pig类

@interface Pig : NSObject

@end

实现Pig类

本例子要验证消息处理查找的流程,修改调用-eat方法的对象:

#import "Pig.h"
#import "Dog.h"

@implementation Pig

// 第一步,我们不动态添加方法,返回NO
+ (BOOL)resolveInstanceMethod:(SEL)sel {
  return NO;
}

// 第二步,备选提供响应aSelector的对象,我们不备选,因此设置为nil,就会进入第三步
- (id)forwardingTargetForSelector:(SEL)aSelector {
  return nil;
}

// 第三步,先返回方法选择器。如果返回nil,则表示无法处理消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  if ([NSStringFromSelector(aSelector) isEqualToString:@"eat"]) {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
  }
  
  return [super methodSignatureForSelector:aSelector];
}

// 第三步,只有返回了方法签名,都会进入这一步,这一步用户调用方法
// 改变调用对象等
- (void)forwardInvocation:(NSInvocation *)anInvocation {
  // 我们改变调用对象为dog
  [anInvocation invokeWithTarget:[[Dog alloc] init]];
}

@end

其实,如果我们调用+ (BOOL)resolveInstanceMethod:(SEL)sel返回NO,那么我们就没有必要实现它,因为默认就是返回NO。

如果调用- (id)forwardingTargetForSelector:(SEL)aSelector返回nil,那么我们也没有必要实现它,因为默认就是返回nil。

但是,对于-methodSignatureForSelector:方法,默认也是返回nil,若不返回某方法签名,那么-forwardInvocation:方法就不会被调用,此时也就崩溃了。

对于-forwardInvocation:方法中,我们修改了调用-eat方法的是实例为Dog实例。

测试例二

由于我们没有声明-eat方法,因此不能通过直接调用。但是,我们可以通过performSelector来实现,当然也可以通过objc_msgSend函数实现:

Pig *pig = [[Pig alloc] init];
[pig performSelector:@selector(eat) withObject:nil afterDelay:0];

通过objc_msgSend也可以实现:

((void (*)(id, SEL))objc_msgSend)((id)pig, @selector(eat));

打印结果却是Dog,说明我们成功地修改调用对象了:

<Dog: 0x7f8d8bf7c510> is eating

例子三

我们这里创建一个Cat类来测试,修改调用方法为其它方法。

声明Cat类

@interface Cat : NSObject

@end

实现Cat类

@implementation Cat

// 第一步:在没有找到方法时,会先调用此方法,可用于动态添加方法
// 我们不动态添加
+ (BOOL)resolveInstanceMethod:(SEL)sel {
  return NO;
}

// 第二步:上一步返回NO,就会进入这一步,用于指定备选响应此SEL的对象
// 千万不能返回self,否则就会死循环
// 自己没有实现这个方法才会进入这一流程,因此成为死循环
- (id)forwardingTargetForSelector:(SEL)aSelector {
  return nil;
}

// 第三步:指定方法签名,若返回nil,则不会进入下一步,而是无法处理消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  if ([NSStringFromSelector(aSelector) isEqualToString:@"eat"]) {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
  }
  
  return [super methodSignatureForSelector:aSelector];
}

// 当我们实现了此方法后,-doesNotRecognizeSelector:不会再被调用
// 如果要测试找不到方法,可以注释掉这一个方法
- (void)forwardInvocation:(NSInvocation *)anInvocation {
  
  // 我们还可以改变方法选择器
  [anInvocation setSelector:@selector(jump)];
  // 改变方法选择器后,还需要指定是哪个对象的方法
  [anInvocation invokeWithTarget:self];
}

- (void)doesNotRecognizeSelector:(SEL)aSelector {
  NSLog(@"无法处理消息:%@", NSStringFromSelector(aSelector));
}

- (void)jump {
  NSLog(@"由eat方法改成jump方法");
}

@end

当我们实现了-doesNotRecognizeSelector:就方法时,就不会因为找不到方法而崩溃了。我们这里将动态地将调用-eat方法修改为-jump方法,同时也要设置这个-jump是哪个对象的。

这里的注释已经说明得很清楚了,就不再细说了!

测试例三

Cat *cat = [[Cat alloc] init];
[cat performSelector:@selector(eat) withObject:nil afterDelay:0];

打印结果为:

由eat方法改成jump方法

说明我们已经成功地动态地修改方法了。

参考源代码:https://github.com/CoderJackyHuang/RuntimeDemo

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 大纲 0.OC数据类型 1.声明一个类 2.实现一个类 3.创建一个对象 4.对象的注意点 5.对象方法 6.类方...
    天天想念阅读 1,096评论 0 3
  • Objective-C Runtime 引言 Objective-C的方法调用实则为“发送消息”,我们来看[dog...
    IOShzz阅读 629评论 0 0
  • Theos安装与配置 Theos是一个越狱开发工具包,使用它可以创建Tweak项目,动态Hook第三方程序。Git...
    乐Coding阅读 7,508评论 5 5
  • 上次提到因为考勤表核对有误,那位分队长与我在群里发生冲突的事情,并且还几次正面交锋,队领导也不公平处理这事,让我很...
    思言悟语阅读 197评论 0 1