我爱编程

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

2018-04-17  本文已影响32人  路漫漫其修远兮Wzt

转载自: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

上一篇 下一篇

猜你喜欢

热点阅读