OC中的消息转发机制

在本文中,将为你解释在OC的动态机制中,一个对象是如何调用,并且在对象中找不到方法的情况下,如何将方法通过"发消息"的形式转发给其它接受者。

在了解消息转发机制之间,要知道OC中方法如何调用,并且为什么需要消息转发。

OC的语法中,我们调用这样调用一个方法:

[receiver method:param];

编译后的c代码是这样调用方法的:

objc_msgSend(receiver, @selector(method), param);
  • 在OC的所有方法中,其实有两个隐藏参数,一个是id receiver规定了方法的接受者,另一个是SEL _cmd,方法的selector。

究竟在msgSend方法中做了什么?

在runtime源码中,msgSend方法在objc-msg-arm.s中,为了保证效率,使用了汇编实现。

id objc_msgSend(id self, SEL _cmd,...);
     ENTRY objc_msgSend 
     MESSENGER_START  
     cbz    r0, LNilReceiver_f ;receiver为空则直接返回
     ldr    r9, [r0]           ;r9 = self->isa 
     CacheLookup NORMAL        ; calls IMP or LCacheMiss
                             ; 首先在缓存中寻找IMP
LCacheMiss:               ; 缓存中找不到IMP 
     MESSENGER_END_SLOW 
     ldr    r9, [r0, #ISA]      ; class = receiver->isa
     b  __objc_msgSend_uncached ;在父类中寻找IMP
LNilReceiver:           ; 找不到IMP,标识IMP为消息转发 
                            ; r0 is already zero 
     mov    r1, #0 
     mov    r2, #0 
     mov    r3, #0 
     FP_RETURN_ZERO 
     MESSENGER_END_NIL 
     bx lr
LMsgSendExit:
     END_ENTRY objc_msgSend

在源码中有清晰的注释,让我们知道,msgSend方法实际上逻辑不复杂。

  • 第一步,判断receiver是否为空,若为空则直接返回,这就是给一个空对象调用方法不会奔溃的原因。
  • 第二步,在缓存中搜索是否已经缓存IMP,若已缓存,则直接返回。
  • 第三步,在父类中寻找IMP方法,找到则返回。* 第四步,标记IMP为_objc_msgForward,意思是调用这个方法直接走消息转发机制。

在类中寻找IMP

在上面的描述中,我们需要在一个类中寻找方法的实现,这个寻找方法,在runtime源码objc-class-old.mm中的方法:

IMP lookUpImpOrForward(Class cls, 
                         SEL sel, 
                          id inst, 
                        bool initialize, 
                        bool cache,
                        bool resolver)

主要执行了以下动作

  1. 解锁methodList,用于无锁查找,速度快。
  2. 在缓存中查找,如果找到,则返回缓存的方法。
  3. 查找cls是否被释放,如果是,则返回cls被释放的错误。
  4. 判断cls是否已经初始化,没有初始化则初始化。
  5. 加锁methodList,防止多线程操作。
  6. 如果是垃圾回收机制的方法,则忽略,并添加到类的忽略方法列表中。
  7. 尝试查找缓存,找到则返回。
  8. 尝试在这个类的方法列表methodList中查找,如果找到,则添加到缓存中。
  9. 如果还没有找到,就从父类的缓存和父类的方法列表中查找,找到就添加到缓存中,没找到就进入_class_resolveMethod方法。
  10. 调用_class_resolveMethod方法,给机会动态添加方法,然后重新调用msgSend查找,看看有没有添加方法。
  11. 将这个方法直接缓存为消息转发。
  12. 解锁methodList。

到此,已经和msgSend没什么关系了,关键在于msgSend方法中,找不到方法的实现,则开始消息转发。

消息转发

上面我们提到,当实在找不到方法实现时,会走消息转发。你有3次机会添加方法的实现,如果还没有方法的实现,程序只能Crash了。接下来用例子分别说明这3次机会。新建一个Son和Father类,并在Son类中添加方法声明,不实现方法。

// Son.h
#import <Foundation/Foundation.h>
@interface Son : NSObject
- (void)method;
@end
// Son.m
#import "Son.h"
#import "Father.h"
#import <objc/runtime.h>
@implementation Son
@end

然后,调用son的method方法。


son调用method奔溃.png

结果可想而知,son的method方法没有实现,并且也没有在消息转发过程中添加实现,所以出现经典的奔溃:

Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[Son method]: unrecognized selector sent to 
instance 0x1003031e0'

那在消息转发中,调用了哪些方法呢?我们给[son method]方法打上断点,并在gbd中输入命令call (void)instrumentObjcMessageSends(YES)

断点调试.png

然后在终端中打开文件夹open /private/tmp找到"msgSend-xxxx"之类的文件,双击打开。
消息转发方法.png

这些就是在消息转发的过程中调用的方法。
1.resolveInstanceMethod: (或 resolveClassMethod:)方法。这是第一次机会让你添加方法实现。在这里,调用class_addMethod方法添加实现,并返回YES,然后会重新开始msgSend流程。如果没有实现,那么进入第二次机会。
2.forwardingTargetForSelector:方法。这是第二次机会,但是这一次不是添加方法实现,而是将消息转发给能够响应此消息的对象,直接把消息发给它。否则返回nil。
3.methodSignatureForSelector:方法。这是第三次机会,这个方法尝试获取方法的签名如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。刚刚奔溃的unrecognized selector sent to instance错误就出自于此。 如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
4.forwardInvocation:方法。将第三步的方法签名添加一个方法实现。
5.doesNotRecognizeSelector: 方法。抛出找不到方法签名的移除,程序Crash。###知道的消息转发的怎么回事,怎么去使用呢?

第一次机会:
+(BOOL)resolveInstanceMethod:(SEL)sel;在son.m中重写resolveInstanceMethod:方法,使用class_addMethod()增加method方法的实现:

// son.m
#import "Son.h"
#import "Father.h"
#import <objc/runtime.h>
@implementation Son
void method(id self, SEL _cmd){ 
     NSLog(@"son method");
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
 if (sel == @selector(method)) { 
     class_addMethod(self, sel, (IMP)method, "v@:" ); 
     return YES;
 }
 return [super resolveInstanceMethod:sel];}@end

可以看到已经成功执行method方法:

第一次机会实现son method.png

第二次机会:
-(id)forwardingTargetForSelector:(SEL)aSelector;

在son.m中重写forwardingTargetForSelector:方法,返回能相应method方法的实例,更换消息的接收者。下面就是让father实例接收method方法:

// son.m
#import "Son.h"
#import "Father.h"
#import <objc/runtime.h>
@implementation Son
-(id)forwardingTargetForSelector:(SEL)aSelector{ 
     return [[Father alloc] init];
}
@end

然后在father.m中实现method方法:

// father.m
#import "Father.h"
@implementation Father
-(void)method{ 
       NSLog(@"Father method");
}
end

惊讶地发现,son实例调用method方法,却是father实现去相应method方法:


第二次机会实现son method.png

第三次机会:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
-(void)forwardInvocation:(NSInvocation *)anInvocation

在son.m中,重写-(NSMethodSignature *)methodSignatureForSelector方法,返回一个method方法的签名,并在forwardInvocation:方法中,指定方法的响应者:

// Son.m
#import "Son.h"
#import "Father.h"
#import <objc/runtime.h>
@implementation Son
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ 
                        NSString *sel = NSStringFromSelector(aSelector); 
                        if([sel isEqualToString:@"method"]){ 
                            //手动为method方法添加签名 
                            return [NSMethodSignature signatureWithObjCTypes:"v@:"]; 
                        } 
                        return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{ 
       SEL selector = [anInvocation selector]; //新建需要接受消息的对象 
       Father *father = [[Father alloc] init]; 
       if([father respondsToSelector:selector]){ 
          //接收对象唤醒方法 
         [anInvocation invokeWithTarget:father]; 
        }
}
@end

最终的结果还是father响应了method方法:

第三次机会实现son method.png

那消息转发有什么用?

在日常开发中,使用消息转发的场景并不多,大多数是防止程序Crash。当然,你也可以直接调用_objc_msgForward,这样会告诉msgSend方法,直接进入消息转发。著名的热修复工具JSPath,就是直接调用_objc_msgForward实现的。

OC中的消息转发大致如此,有什么问题可以留言,我们共同探讨哦。

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 ...
    lylaut阅读 1,811评论 2 3
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,569评论 18 139
  • 云计算Cloud Computing 这一术语,意味着强大、高速、可扩展性。实际上,云相当于那些拥有无限扩展能力...
    不羁小生阅读 160评论 0 0
  • 2015年9月 我在黄州区西湖中学度过了三年初中生涯。离开家乡后才知道原来我的初中校园面积这么小,办学条件自然不必...
    HfzCh阅读 1,912评论 2 4