iOS Runtime 消息转发机制原理和实际用途

写这篇文章的起因:

从一个对象收到一个它无法响应的方法到崩溃之间发生了什么?
这是J_Knight在最近在博客里面问到的一个问题。其实本质上是在问iOS的消息转发机制。类似的原理文章有很多,但大多数都是在单纯的讲原理,并没有讲解实际的用处。本文先对iOS的消息转发机制进行一个全面的原理讲解,并且在后面起一个引子告诉大家一些通过这个原理可以用来实现的功能,通过这些用途,可以更深刻的理解消息转发机制的本质,让我们能对费了很长时间理解的知识点的价值得更全面的认识。因为东西搞懂了是来用的,单纯的知道原理并不会对自身的提升太高的价值。

分析问题:

下面我会配合一个DEMO来做讲解

LMMessageForwardDemo Demo GitHub地址

我们在开发过程中,经常会遇到这样的报错
当我们调用一个对象不存在的方法的时候

@property (nonatomic,strong) TestMessage* test;
[self.test performSelector:@selector(testFunction)];

系统报错 提示如下错误

[TestMessage testFunction]: unrecognized selector sent to instance 0x1c4015330

类似的报错都是iOS的消息转发机制在无法响应方法之后抛出的问题,我们下面就看一下消息转发机制

原理:

我们先看一下下面这个结构图,先对整个消息处理机制有一个初步的认识,很多讲解的过程中也都有类似的结构图,纯粹通过结构图来理解是不够的。


iOS消息转发机制.jpeg

从全局来看,消息转发机制共分为3大步骤:
1.Method resolution 方法解析处理阶段
2.Fast forwarding 快速转发阶段
3.Normal forwarding 常规转发阶段

那么如果想要不抛出unrecognized selector 的报错,也就需要从这3步里面来做补救了,我们一步一步来看如何在这3个阶段来进行补救。

第一步:Method resolution 方法解析处理阶段

如果调用了对象方法首先会进行+(BOOL)resolveInstanceMethod:(SEL)sel判断
如果调用了类方法 首先会进行 +(BOOL)resolveClassMethod:(SEL)sel判断
两个方法都为类方法,如果YES则能接受消息 NO不能接受消息 进入第二步

我们先调用一下对象方法

 [self.test performSelector:@selector(testFunction)];

然后在resolveInstanceMethod进行补救,这里用到了我封装的一个Runtime工具类,这里暂时不做展开讲解,会在后续其他文章里面展开讲解,功能是对一个类添加一个方法。

+(BOOL)resolveInstanceMethod:(SEL)sel{
    //判断是否为外部调用的方法
    if ([NSStringFromSelector(sel) isEqualToString:@"testFunction"]) {
        /**
         对类进行对象方法 需要把方法添加进入类内
         */
        [LMRuntimeTool addMethodWithClass:[self class] withMethodSel:sel withImpMethodSel:@selector(addDynamicMethod)];
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

下面我们再来调用一下TestMessage的类方法

[[TestMessage class] performSelector:@selector(testClassFunction)];

如果调用类方法需要在resolveClassMethod 进行补救判断

+(BOOL)resolveClassMethod:(SEL)sel{
    if ([NSStringFromSelector(sel) isEqualToString:@"testClassFunction"]) {
        /**
         对类进行添加类方法 需要讲方法添加进入元类内
         */
        [LMRuntimeTool addMethodWithClass:[LMRuntimeTool getMetaClassWithChildClass:[self class]] withMethodSel:sel withImpMethodSel:@selector(addClassDynamicMethod)];
        return YES;
    }
    return [super resolveClassMethod:sel];
}

这里有一个需要特别注意的地方,类方法需要添加到元类里面,OC中所有的类本质上来说都是对象,对象的isa指向本类,类的isa指向元类,元类的isa指向根元类,根元类的isa指向自己,这样的话就形成了一个闭环。
[LMRuntimeTool getMetaClassWithChildClass:[self class]] 这个方法是用来获取本类的元类,对元类添加需要添加的方法。

经过上面两种类型的补救,果然对象方法和类方法都不在抛出异常了,并且打印了数据

2018-08-06 15:25:25.667572+0800 MessageForwardDemo[3599:949889] 动态添加类方法
2018-08-06 15:25:25.667612+0800 MessageForwardDemo[3599:949889] 动态添加方法

第二步:Fast forwarding 快速转发阶段 (后面阶段都针对对象来处理,不考虑类方法)

如果在上一步的2个方法内返回的为YES则能接受消息 NO不能接受消息 进入第二步,我们先把上面方法内的处理方案注释掉,让消息转发进入第二步。
我们新创建一个BackupTestMessage类,里面声明和实现testFunction方法,用来当作备用响应者。

-(id)forwardingTargetForSelector:(SEL)aSelector{
  if ([NSStringFromSelector(aSelector) isEqualToString:@"testFunction"]) {
        return [BackupTestMessage new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

因为一个对象内部可能还有其他可能响应的对象,所以这个方法是转发SEL去对象内部的其他可以响应该方法的对象。
这里创建的一个BackupTestMessage的实例内定义的有testFunction方法,所以返回这个实例之后,果然不再报错了,并且根据打印也能看得出来走了BackupTestMessage这个类的实例方法

2018-08-06 15:27:43.234733+0800 MessageForwardDemo[3629:951288] 备用类的对象方法testFunction

已经让备用的对象去响应了TestMessage本身无法响应的一个SEL

第三步:Normal forwarding 常规转发阶段

如果第2步返回self或者nil,则说明没有可以响应的目标 则进入第三步。
第三步的消息转发机制本质上跟第二步是一样的都是切换接受消息的对象,但是第三步切换响应目标更复杂一些,第二步里面只需返回一个可以响应的对象就可以了,第三步还需要手动将响应方法切换给备用响应对象。
第三步有2个步骤:

(1)-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

在第(1)步中,返回SEL方法的签名,返回的签名是根据方法的参数来封装的。
1.手动创建签名 但是尽量少使用 因为容易创建错误 可以按照这个规则来创建
https://blog.csdn.net/ssirreplaceable/article/details/53376915
根据OBJC的编码类别进行编写后面的char (但是容易写错误,所以建议使用下面的方法)
NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
//写法例子
//例子"v@:@"
//v@:@ v:返回值类型void;@ id类型,执行sel的对象;:SEL;@参数
//例子"@@:"
//@:返回值类型id;@id类型,执行sel的对象;:SEL
2.自动创建签名
BackupTestMessage * backUp = [BackupTestMessage new];
NSMethodSignature * sign = [backUp methodSignatureForSelector:aSelector];
使用对象本身的methodSignatureForSelector自动获取该SEL对应类别的签名

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    //如果返回为nil则进行手动创建签名
   if ([super methodSignatureForSelector:aSelector]==nil) {
        NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return sign;
    }
    return [super methodSignatureForSelector:aSelector];
}
(2)-(void)forwardInvocation:(NSInvocation *)anInvocation

上方的第(1)步中如果调用返回有签名 则进入消息转发最后一步

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    //创建备用对象
    BackupTestMessage * backUp = [BackupTestMessage new];
    SEL sel = anInvocation.selector;
    //判断备用对象是否可以响应传递进来等待响应的SEL
    if ([backUp respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:backUp];
    }else{
       // 如果备用对象不能响应 则抛出异常
        [self doesNotRecognizeSelector:sel];
    }
}

在三个步骤的每一步,消息接受者都还有机会去处理消息。同时,越往后面处理代价越高,最好的情况是在第一步就处理消息,这样runtime会在处理完后缓存结果,下回再发送同样消息的时候,可以提高处理效率。第二步转移消息的接受者也比进入转发流程的代价要小,如果到最后一步forwardInvocation的话,就需要处理完整的NSInvocation对象了。

实际用途:

1.JSPatch --iOS动态化更新方案

具体实现bang神已经在下面两篇博客内进行了详细的讲解,非常精妙的使用了,消息转发机制来进行JS和OC的交互,从而实现iOS的热更新。虽然去年苹果大力整改热更新让JSPatch的审核通过率在有一段时间里面无法过审,但是后面bang神对源码进行代码混淆之后,基本上是可以过审了。不论如何,这个动态化方案都是技术的一次进步,不过目前是被苹果爸爸打压的。不过如果在bang神的平台上用正规混淆版本别自己乱来,通过率还是可以的。有兴趣的同学可以看看这两篇原理文章,这里只摘出来用到消息转发的部分。

http://blog.cnbang.net/tech/2808/
http://blog.cnbang.net/tech/2855/

bang神博客相关消息转发内容.png

具体的实现原理可以去bang神的博客查看。

2.为 @dynamic 实现方法

使用 @synthesize 可以为 @property 自动生成 getter 和 setter 方法(现 Xcode 版本中,会自动生成),而 @dynamic 则是告诉编译器,不用生成 getter 和 setter 方法。当使用 @dynamic 时,我们可以使用消息转发机制,来动态添加 getter 和 setter 方法。当然你也用其他的方法来实现。

3.实现多重代理

利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。

https://blog.csdn.net/kingjxust/article/details/49559091

4.间接实现多继承

Objective-C本身不支持多继承,这是因为消息机制名称查找发生在运行时而非编译时,很难解决多个基类可能导致的二义性问题,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。

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

推荐阅读更多精彩内容