Runtime系列5之-消息转发

这一章,我们就要开始讨论Runtime中最有意思的一部分:消息处理机制。我们将详细讨论消息的发送及消息的转发。其实这个也应该是方法的一部分,因为方法就是消息.

方法调用

先了解一下方法调用相关的知识

方法调用流程

  1. 在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。这个函数将消息接收者和方法名作为其基础参数,如以下所示:

     objc_msgSend(receiver, selector)
    
  2. 如果消息中还有其它参数,则该方法的形式如下所示:

     objc_msgSend(receiver, selector, arg1, arg2, ...)
    
  3. 这个函数完成了动态绑定的所有事情:
    1. 首先它找到selector对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。
    2. 它调用方法实现,并将接收者对象及方法的所有参数传给它。
    3. 最后,它将实现返回的值作为它自己的返回值。
  4. 消息的关键在于我们前面章节讨论过的结构体objc_class,这个结构体有两个字段是我们在分发消息的关注的:
    1. 指向父类的指针
    2. 一个类的方法分发表,即methodLists。
  5. 当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。
  6. 下图演示了这样一个消息的基本框架:

    pic -w90

    1. 当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程,这个我们在后面讨论。

隐藏参数

  1. objc_msgSend有两个隐藏参数:
    1. 消息接收对象
    2. 方法的selector
  2. 这两个参数为方法的实现提供了调用者的信息。之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明。它们是在编译期被插入实现代码的。

获取方法地址

  1. Runtime中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得那么直接。当然,方法的缓存一定程度上解决了这一问题。
  2. 我们上面提到过,如果想要避开这种动态绑定方式,我们可以获取方法实现的地址,然后像调用函数一样来直接调用它。特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种方式可以提高程序的性能。
  3. NSObject类提供了methodForSelector:方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。
  4. 我们通过以下代码来看看methodForSelector:的使用:

     //定义一个函数指针
     void (*setter)(id, SEL, BOOL);
     int i;
     //拿到一个OC方法的函数指针
     setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
     //直接调用C函数
     for (i = 0 ; i < 1000 ; i++)
         setter(targetList[i], @selector(setFilled:), YES);
    
  5. 这里需要注意的就是函数指针的前两个参数必须是id和SEL。
  6. 当然这种方式只适合于在类似于for循环这种情况下频繁调用同一方法,以提高性能的情况。另外,methodForSelector:是由Cocoa运行时提供的;它不是Objective-C语言的特性。

消息转发

  1. 当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?
  2. 默认情况下,如果是以[object message]的方式调用方法,如果object没有message消息声明时,编译器会报错。但如果有了声明,就同perform...的形式来调用一样,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。抛出 unrecognized selector sent to …类似这样的异常信息.
  3. 但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。(后两种为消息转发)
    1. Method Resolution
    2. Fast Forwarding
    3. Normal Forwarding
  4. 通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:

     if ([self respondsToSelector:@selector(method)]) {
         [self performSelector:@selector(method)];
     }
    
  5. 不过,我们这边想讨论下不使用respondsToSelector:判断的情况。这才是我们这一节的重点。

Method Resolution

  1. 首先Objective-C在运行时调用+ resolveInstanceMethod:+ resolveClassMethod:方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。
  2. 举一个简单例子,定义一个类Message,它主要定义一个方法sendMessage,下面就是它的设计与实现:

     @interface Message : NSObject
     - (void)sendMessage:(NSString *)word;
     @end
        
     @implementation Message
     - (void)sendMessage:(NSString *)word
     {
         NSLog(@"normal way : send message = %@", word);
     }
     @end
    
    1. 如果我在viewDidLoad方法中创建Message对象并调用sendMessage方法:

       - (void)viewDidLoad {
           [super viewDidLoad];
           Message *message = [Message new];
           [message sendMessage:@"Sam Lau"];
       }
      

      打印以下信息: normal way : send message = Sam Lau

    2. 但现在我将原来sendMessage方法实现给注释掉,覆盖resolveInstanceMethod方法:

       #pragma mark - Method Resolution
       /// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation
       + (BOOL)resolveInstanceMethod:(SEL)sel
       {
           if (sel == @selector(sendMessage:)) {
               class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) {
                   NSLog(@"method resolution way : send message = %@", word);
               }), "v@*");
           }
           return YES;
       }
      

      打印以下信息:method resolution way : send message = Sam Lau

    3. 如果resolveInstanceMethod方法返回NO,运行时就跳转到下一步:消息转发(Message Forwarding),下面两种皆为消息转发

Fast Forwarding

  1. 如果目标对象实现- forwardingTargetForSelector:方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding
  2. 继续上面Message类的例子,将sendMessageresolveInstanceMethod方法注释掉,然后添加forwardingTargetForSelector方法的实现:

     #pragma mark - Fast Forwarding
     - (id)forwardingTargetForSelector:(SEL)aSelector {
         if (aSelector == @selector(sendMessage:)) {
             return [MessageForwarding new];
         }
         return nil;
     }
    
  3. 此时还缺一个转发消息的类MessageForwarding,这个类的设计与实现如下:

     @interface MessageForwarding : NSObject
     - (void)sendMessage:(NSString *)word;
     @end
     @implementation MessageForwarding
     - (void)sendMessage:(NSString *)word
     {
         NSLog(@"fast forwarding way : send message = %@", word);
     }
     @end
    

    打印以下信息:fast forwarding way : send message = Sam Lau

  4. 这里叫Fast,是因为这一步不会创建NSInvocation对象,但Normal Forwarding会创建它,所以相对于更快点。

Normal Forwarding

  1. 如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用methodSignatureForSelector:方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:方法。
  2. 继续前面的例子,将forwardingTargetForSelector方法注释掉,添加methodSignatureForSelectorforwardInvocation方法的实现:

     #pragma mark - Normal Forwarding
     - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
     {
         NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
         if (!methodSignature) {
             methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
         }
         return methodSignature;
     }
     - (void)forwardInvocation:(NSInvocation *)anInvocation
     {
         MessageForwarding *messageForwarding = [MessageForwarding new];
         if ([messageForwarding respondsToSelector:anInvocation.selector]) {
             [anInvocation invokeWithTarget:messageForwarding];
         }
     }
    

三种方法的选择

  1. Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?
    1. Method Resolution:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。
    2. Fast Forwarding:它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。
    3. Normal Forwarding:它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。

消息转发与多重继承

  1. 回过头来看第二(Fast Forwarding)和第三步(Normal Forwarding),通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。
  2. 不过消息转发虽然类似于继承,但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; 	
     }
    

举例: 动态添加方法

  1. 如果一个类方法非常多,其中可能许多方法暂时用不到。而加载类方法到内存的时候需要给每个方法生成映射表,又比较耗费资源。此时可以使用RunTime动态添加方法;
  2. 动态给某个类添加方法,相当于懒加载机制,类中许多方法暂时用不到,那么就先不加载,等用到的时候再去加载方法。
  3. 动态添加方法的方法:
    1. 首先我们先不实现对象方法,当调用performSelector: 方法的时候,再去动态加载方法。
    2. 创建Person类,使用performSelector: 调用Person类对象的eat方法。
    3. 此时编译的时候是不会报错的,程序运行时才会报错,因为Person类中并没有实现eat方法,当去类中的Method List中发现找不到eat方法,会报错找不到eat方法。
    4. 而当找不到对应的方法时就会来到拦截调用,在找不到调用的方法程序崩溃之前调用的方法。
    5. 当调用了没有实现的对象方法的时,就会调用+(BOOL)resolveInstanceMethod:(SEL)sel方法。
    6. 当调用了没有实现的类方法的时候,就会调用+(BOOL)resolveClassMethod:(SEL)sel方法。
    7. 我们可以实现方法resolveInstanceMethod:或者resolveClassMethod:方法,动态的给实例方法或者类方法添加方法和方法实现。
    8. 所以通过这两个方法就可以知道哪些方法没有实现,从而动态添加方法。参数sel即表示没有实现的方法。
    9. 一个objective - C方法最终都是一个C函数,默认任何一个方法都有两个参数。self : 方法调用者 _cmd : 调用方法编号。我们可以使用函数class_addMethod为类添加一个方法以及实现。

class_addMethod函数

  1. 举例之前我们先详细的讲一下这个函数

     class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
    
    1. class_addMethod中的四个参数。第一,二个参数比较好理解,重点是第三,四个参数。
    2. cls : 表示给哪个类添加方法,这里要给Person类添加方法,self即代表Person。
    3. SEL name : 表示添加方法的编号。因为这里只有一个方法需要动态添加,并且之前通过判断确定sel就是eat方法,所以这里可以使用sel。
    4. IMP imp : 表示方法的实现,函数入口,函数名可与方法名不同(建议与方法名(SEL中的name)相同)需要自己来实现这个函数。每一个方法都默认带有两个隐式参数 self : 方法调用者 _cmd : 调用方法的标号,可以写也可以不写。
    5. types : 表示方法类型,需要用特定符号。系统提供的例子中使用的是"v@:",我们来到API中看看"v@:"指定的方法是什么类型的。
      1. 从苹果官方的Type Encoding可以看出:
        1. v -> void 表示无返回值
        2. @ -> object 表示id参数
        3. : -> method selector 表示SEL

动态创建无参数的方法

//Person.h文件
#import <Foundation/Foundation.h>

@interface Person : NSObject
@end

//Person.m文件
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
/*
 override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel
{

    // 动态添加eat方法
    // 首先判断sel是不是eat方法 也可以转化成字符串进行比较。
    if (sel == @selector(eat)) {
         class_addMethod(self, sel, (IMP)eat , "v@:");
        // 处理完返回YES
        return YES;
    }
     return [super resolveInstanceMethod:sel];
}
/*
 这个是C函数,函数名就是函数地址,即指针
 IMP也是函数指针因此,这个函数的IMP就是(IMP)eat
 */
void eat(id self ,SEL _cmd)
{
    // 实现内容
    NSLog(@"%@的%@方法动态实现了",
          self,NSStringFromSelector(_cmd));
}
@end
//调用:
 Person *p = [[Person alloc]init];
 // 当调用 P中没有实现的方法时,动态加载方法
[p performSelector:@selector(eat)];

动态创建有参数的方法

  1. 如果是有参数的方法,需要对方法的实现和class_addMethod方法内方法类型参数做一些修改。
  2. 方法实现:因为在C语言函数中,所以对象参数类型只能用id代替。
  3. 方法类型参数:因为添加了一个id参数,所以方法类型应该为”v@:@”
// Person.m文件
 #import "Person.h"
#import <objc/runtime.h>
@implementation Person
+(BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat:)) {
        class_addMethod(self, sel, (IMP)aaaa , "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void aaaa(id self ,SEL _cmd,id Num)
{
    // 实现内容
    NSLog(@"%@的%@方法动态实现了,参数为%@",self,NSStringFromSelector(_cmd),Num);
}
@end
//调用函数
//为参数@"xx_cc"
    Person *p = [[Person alloc]init];
    [p performSelector:@selector(eat:)withObject:@"xx_cc"];  
     

注意:我们看到person.h的文件中我们并没有方法-(void)eat;的声明,所以,如果我们用[p eat];,在编译期就会报错,但是如果我们用[p performSelector:@selector(eat)];只会有警告,在点击调用时,才会去查方法的实现.

Table of Contents