OC学习Runtime之消息传递,消息转发机制

转载自:OC学习Runtime之消息传递,消息转发机制

参考文献:Effective Objective-C Notes:理解消息传递机制
ARC 下内存泄露的那些点
weak 弱引用的实现方式
iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析)

相关类和函数

介绍消息发送机制之前介绍一下会用到的几个相关类和函数

NSMethodSignature(方法签名)

方法签名:用语记录一个方法的参数和返回值类型的类。类似于objc_method_description结构体。方法签名是用于初始化NSInvocation用的。

NSInvocation

用于记录一个消息(方法)的接收者(target)方法名(SEL)参数类型,参数等信息,包含执行该消息方法的类。
此类类似于结构体Method。
他提供了- (void)invokeWithTarget:(id)target;调用对象中的方法。类似于id method_invoke(id receiver, Method m, …) 函数。

注意:NSMethodSignature,NSInvocation一般我们都是用语消息转发时候用到。正常我们用的比较少。

objc_msgSend 函数

objc_msgSend函数负责像某对象发送一个消息。定义如下

id objc_msgSend(id self, SEL op, ...)

在OC里面我们调用对象方法[Receiver message]的这种模式,实际是通过调用objc_msgSend(Receiver,message,…)函数来找到方法的实现入口。objc_msgSend实现原理是通过对象对应的objc_object的ISA找到该类对应的objc_class结构体。通过依次遍历objc_cache,objc_method_list里面的方法找到方法实际入口,如果没有找到,则跳到父类寻找,以此类推如果最终都没有找到就会发生下面介绍的消息转发机制。

消息转发机制

Runtime中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提 升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得那么直接。当然,方法的缓存一定程度上解决了这一问题。

特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种直接调用IMP减少查找过程的方式可以提高程序的性能。NSObject类提供了methodForSelector:方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。

当我们像一个对象发送消息[Receiver message],Receiver没有实现该消息,即[Receiver respondsToSelector:SEL]返回为NO情况下,其实系统不会立刻出现cash,这时Runtime system会对message进行转发。转发之后,如果该消息依然没有被执行就会出现Cash!Runtime System为我们提供了三种解决这种给对象发送没有实现消息方案。
消息转发机制基本上分为三个步骤:
1. 动态方法解析
2. 备用接收者
3. 完整转发
我们可以通过控制这三个步骤其中一环来解决这一个问题

特别注意:如果是正常类的消息,是不会走到这三个步骤的。所以走到这三个不步骤的前提条件已经确定该消息为未知消息

测试用例用到的源码
Boy.h

#import <Foundation/Foundation.h>

@interface Boy : NSObject
-(void)say:(NSString*)str girl:(NSString*)girl;
@end

Boy.m

#import "Boy.h"
#import "Girl.h"
@implementation Boy
-(void)say:(NSString*)str girl:(NSString*)girl
{
    NSLog(@"%@",str);
    NSLog(@"%@",girl);

}
@end

Girl.h

#import <Foundation/Foundation.h>

@interface Girl : NSObject
-(void)sayGirl:(NSString*)word;
@end

Girl.m

#import "Girl.h"
#import <objc/runtime.h>
void dynamicMethodIMP(id self, SEL _cmd)
{
    NSString *className = NSStringFromClass([self class]);
    NSString *selName = NSStringFromSelector(_cmd);
    NSLog(@"%@:不是%@的方法",className,selName);//给用户警告但是不cash
}
@implementation Girl
-(void)sayGirl:(NSString*)word
{
    NSLog(@"I am a girl");
}
@end
动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或 者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法”“。不过使用该方法的前提是我们已经 实现了该”处理方法”,我们可以在运行时通过class_addMethod函数动态添加未知消息到类里面,让原来没有处理这个方法的类具有了处理这个方法的能力。

实例:

    Girl *girl = [[Girl alloc] init];
    [girl performSelector:@selector(sayGirl:) withObject:nil];
    [girl performSelector:@selector(checkBoy) withObject:nil];

输出:

2015-09-22 15:14:58.619 Runtime[24078:1202259] I am a girl
2015-09-22 15:14:58.619 Runtime[24078:1202259] -[Girl checkBoy]: unrecognized selector sent to instance 0x100203cb0

可以看出系统cash一条unrecognized selector的异常。
现在我们在Girl.m实现+(BOOL)resolveInstanceMethod:(SEL)sel方法。类方法也类似。

+(BOOL)resolveInstanceMethod:(SEL)sel
{

    //从系统里匹配SEL,如没有就注册SEL
    SEL systemSel = sel_registerName(sel_getName(sel));
    //把所有未知的SEL指向dynamicMethodIMP的实现,让dynamicMethodIMP帮忙打印错误信息,但是程序不会Cash
    class_addMethod(self,systemSel,(IMP)dynamicMethodIMP,"v@:");
    return [super resolveClassMethod:systemSel];
}

再运行上面的实例代码时候就输出正常,并切不会cash
输出:

2015-09-22 15:17:25.639 Runtime[24223:1212399] I am a girl
2015-09-22 15:17:25.640 Runtime[24223:1212399] checkBoy:不是Girl的方法

对于dynamicMethodIMP里面处理的事情可以是提醒,也可以把这条消息转发给其他对象等。

备用接收者

如果在动态方法解析无法处理消息,则Runtime会继续调以下方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

如果一个对象实现了这个方法,并返回一个非nil的对象,则返回的对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。
在Boy.m里面添加如下代码

-(id)forwardingTargetForSelector:(SEL)aSelector
{
    return [[Girl alloc] init];
}

实例:

Boy *boy = [[Boy alloc] init];
[boy performSelector:@selector(sayGirl:) withObject:nil];//发一条girl的消息
[boy performSelector:@selector(checkBoy) withObject:nil];//发一条两个对象都无法识别的消息,由于girl可以接收任意消息所以这里也不会cash

输出:

2015-09-22 15:26:21.974 Runtime[24746:1250012] I am a girl  //成功转发了消息
2015-09-22 15:26:21.974 Runtime[24746:1250012] checkBoy:不是Girl的方法 
完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
此时会调用以下方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation

由于NSInvocation的初始化需要有一个方法签名NSMethodSignature,所以我们还需要实现下面的方法,该方法的返回值用于初始化NSInvocation的。

-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector

所以我们在Boy.m里面实现这两个函数,在实现这两个函数之前注释掉上一步添加的

-(id)forwardingTargetForSelector:(SEL)aSelector
{
    return [[Girl alloc] init];
}

因为如果在上一步中如果未知消息被处理就不会走到这里了,删除完之后在Boy.m里添加代码如下

//在这里产生方法签名,以确保NSInvocation能被转发的Girl类执行,不然的话会出现错误
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([Girl instancesRespondToSelector:aSelector]) {
            signature = [Girl instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([Girl instancesRespondToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:[[Girl alloc] init]];//将消息转发给Girl对象
    }
}

实例:

    [boy performSelector:@selector(sayGirl:) withObject:nil];
    [boy performSelector:@selector(checkBoy) withObject:nil];

输出:

2015-09-23 09:31:50.388 Runtime[3583:151342] I am a girl
2015-09-23 09:31:50.388 Runtime[3583:151342] checkBoy:不是Girl的方法

输出结果与备用接收者那一步一样,表示我们转发成功
NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。
从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。
消息转发与多重继承
回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这 种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能 集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转 发。
不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:和isKindOfClass:只能用于继承体系,而不能用于转发链。便如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:

- (BOOL)respondsToSelector:(SEL)aSelector   {
       if ( [super respondsToSelector:aSelector] )
                return YES;     
       else {
                 /* Here, test whether the aSelector message can
                  *            
                  * be forwarded to another object and whether that  
                  *            
                  * object can respond to it. Return YES if it can.  
                  */      
       }
       return NO;  
}

小结
在此,我们已经了解了Runtime中消息发送和转发的基本机制。这也是Runtime的强大之处,通过它,我们可以为程序增加很多动态的行为,虽 然我们在实际开发中很少直接使用这些机制(如直接调用objc_msgSend),但了解它们有助于我们更多地去了解底层的实现。其实在实际的编码过程中,我们也可以灵活地使用这些机制,去实现一些特殊的功能,如hook操作等。

版权声明:本文为博主原创文章,欢迎转载。 https://blog.csdn.net/u014410695/article/details/48650965

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

推荐阅读更多精彩内容

  • http://blog.csdn.net/u014410695/article/details/48650965
    陈阿琦阅读 214评论 0 0
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,529评论 33 466
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 继上Runtime梳理(四) 通过前面的学习,我们了解到Objective-C的动态特性:Objective-C不...
    小名一峰阅读 737评论 0 3
  • 2017年5月26日 星期五 晴 小宝35周+5天 日记一放就是九天,愈发不知如何下笔了。 当我因为夫君熬夜不回而...
    朱砂紅塵阅读 377评论 0 1