Runtime

266 阅读21分钟

Runtime

虽然平时在工作中,比较少利用到Runtime的知识,但是,runtime的知识对于我们来说,却是必备的技能包之一。在开发iOS App的时候,基本上每个人都接触过[object doSomething]这样的方法调用方式。通过[]来进行某个实例对象或者类对象的方法。不过,可能在学习OC的时候,可能都会忽略“发送消息”这样的字眼。在OC中,方法的调用,实际上就是消息的发送与接收。[object doSomething]的意思就是,给object这个对象(object作为接收者),发送一个doSomething的消息。 换句话说,OC在编译阶段会确定消息的内容与其接收者,但是直到在运行过程中,才会确定接收者会如何处理该消息:是直接执行?转发给其他对象?还是完全不知道这个消息是啥东东,直接crash?这决定性的时刻,都是发生在Runtime的过程中。So,我们可以看到,Runtime是如此滴重要。

与Runtime进行交互

Objective-C的程序能够通过下面三种方式与Runtime进行交互:

  • OC源码
  • Foundation框架中的NSObject的方法
  • 直接调用runtime的函数

OC源码

对于大多数情况下,runtime的系统会在幕后自动帮我们完成这部分工作。我们只需要关心OC代码的编写和编译过程即可。 当你编译OC的类和方法时,编译器会根据OC的动态特性来创建相关的数据结构和函数调用。数据结构会获取类的信息,category的定义和协议的声明,详细一点来说,就是method selectors, instance variable templates等。

NSObject方法

基本上Cocoa中的对象都是继承自NSObject,大多数对象也都会继承NSObject的方法(NSProxy这个类比较特殊)。 有的NSObject的方法可以查询runtime系统的一些信息,通过这些方法来实现对象的自省。比方说isKindOfClass:isMemberOfClass:,会检测对象是否在当前的继承体系中;respondsToSelector:表示对象是否能接收响应这个消息;conformsToProtocol:表示对象是否实现了指定的协议;methodForSelector:则提供了方法的实现的地址(函数指针)

Runtime函数

Runtime系统的库能在/usr/include/objc的目录中找到。

Runtime相关的数据结构

接下来我们说下Runtime相关的数据结构。还是从objc_msgSend说起。

从Runtime的源码中找到objc_msgSend的定义如下:

void objc_msgSend(void /* id self, SEL op, ... */ )

id

id类型相信大家都已经很熟悉了,在OC中,基本上对象都能用id进行表示,因为OC直到runtime的时候才能确认一个对象的类型,所以id类型基本上能说“万金油”类型吧。 实际上id是一个指向类实例的指针:typedef struct objc_object *id 好奇宝宝又要发问了:"乜嘢系objc_object"?在Apple文档中查的话,我们能看到objc_object的结构是:

struct objc_object {
   Class isa;
};

objc_object是一个含向类对象的isa指针,文档对该结构体的说明如下:

When you create an instance of a particular class, the allocated memory contains an objc_object data structure, which is directly followed by the data for the instance variables of the class. The alloc and allocWithZone: methods of the Foundation framework class NSObject use the function class_createInstance to create objc_object data structures.

当你创建类的实例对象时,已分配的内存在实例变量的数据后面,会包含有一个objc_object的数据结构。Foundation框架中的NSObjectallocallocWithZone:方法会使用class_createInstance来创建objc_object这个结构体。

也正是因为这个isa指针,所以在runtime的过程中,我们能通过isa指针指向的类对象来确定实例的类型。

这篇文章中看到一个提醒,就是isa指针并不一定会指向实例所属的类, 这里需要注意下KVO的问题。先记下来,等之后学习再查看

SEL

在OC中,我们一般使用@selector来获取对应的方法,即SEL类型。SEL的实际定义为: typedef struct objc_selector *SEL;, 代表了一个方法选择器(method selector)

runtime的源码中,对objc_selector并没有过多的详细的描述。但是我在Stack Overflow的How do SEL and @selector work?找到了答案:

SEL实际上可以看做是存储了方法名称(C的字符串)的const char [] 举个🌰, @selector(instanceMethod)这句代码创建了字符串"instanceMethod", 然后将字符串传递给OC的runtime,runtime将其转换为唯一能够响应这个字符串的的指针,大概就是: SEL sel = @selector(instanceMethod); SEL sel = sel_registerName("instanceMethod"); 更详细的过程需要查看runtimede的sel_getNamesel_registerName

Class

回到上面的isa指针,你会发现isa指针的类型是Class。还是看源码:

typedef struct objc_class *Class;
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

(id -> objc_object(这里是Class isa) -> objc_class, 应该就是id最终指向的是objc_class这个结构体的意思吗?)所以在OC中的所有对象都是由objc_class构成,包括类,类也是一个对象。 根据下面这张图片来看的话

Class
实例对象的isa指针始终指向其类对象,类对象的isa指针指向元类(meta class), super_class指向超类,元类的super_class也是指向其父元类。所有对象最后都是指向NSObject,有趣的是,NSObject的元类是自己(可以用objc_getMetaClass试一试), 超类为nil。

好了,回到objc_class这个结构上 OBJC2_UNAVAILABLE表明的是objc2.0以后就不适用。虽然如此,但是为了兼容新版本,我们还是能从这么点内容中看看实例对象的结构。 从源码上,我们能看到该结构存储了超类,实例变量,方法列表,缓存和所遵循的协议。

objc_ivar_list

objc_ivar_list指的是成员变量列表

struct objc_ivar {
    char *ivar_name                                          OBJC2_UNAVAILABLE;
    char *ivar_type                                          OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}                                                            OBJC2_UNAVAILABLE;

struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

objc_class存储的是指向成员列表的指针,简单理解就是存储了objc_ivar的结构体,而objc_ivar代表了变量的名称,属性,偏移量和位置。因为在runtime中成员变量列表的变量大小,内存和位置已经确定,所以我们没办法在运行过程中给类动态添加变量。如果是通过Category来添加的话,并不是对成员变量列表进行修改来达到目的。

objc_method_list

objc_method_list指的是方法列表

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

objc_class中,是该类型的变量是一个指向指针的指针,这也是为什么我们能动态添加方法,不能动态添加属性的原因。一个是指向列表的指针,一个是指向列表的指针的指针。简单来说,objc_method_list就是存储了objc_method的列表,objc_method就是方法的相关信息。

objc_cache

从名字可以看出,这是一个缓存用的:

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

这是Apple为了提高效率而提供的。在实例对象接收到一个消息时,会首先在缓存区寻找方法的实现,如果找不到的话,才会在dispatch table中根据isa的指向来查找方法实现,从而提高执行效率。Runtime会将最近被调用的方法存储到cache中。

objc_protocol_list

struct objc_protocol_list {
    struct objc_protocol_list *next;
    long count;
    __unsafe_unretained Protocol *list[1];
};

这个指针代表的是objc_protocol_list这个协议列表,表示类所实现的协议

消息

objc_msgSend

在OC中,只有到了runtime的时候,消息才会和方法的实现进行绑定。在这个过程中,编译器会将消息表达式[receiver message]转化为消息函数objc_msgSend.该函数带有两个参数,一个是消息的接收者,另一个是在方法调用时用到的方法名称,也就是method selector:

objc_msgSend(receiver, selector)

另外,如果消息表达式是带参数的话,那么消息函数看起来就是这样的:

objc_msgSend(receiver, selector, arg1, arg2, ...)

消息函数通过下面的步骤来进行动态绑定:

  1. 找到selector的方法实现。由于不同的类可能会实现同一个方法,所以是基于接收者的类型进行方法实现的查找的。
  2. 找到实现后,调用之。将实现接收者对象(指针)和参数发送给方法实现
  3. 最后,将实现过程的返回值作为消息的返回值进行返回

消息机制的关键在于编译器为每个类和对象所创建的结构体。每个类的结构体包含了两个最基本的元素:

  • 指向超类的指针
  • 类分发表(dispatch table)

分发表将方法选择器和方法实现的地址进行连接。比如,选择器的setOrigin::方法和setOrigin::的实现相关联。 当一个对象被创建,分配内存,初始化实例变量的时候,首先出现在对象的变量中的是指向类结构的指针,这就是我们上面说的isa。通过isa指针和其类,超类以及继承结构中的类进行相联系。

Messaging1

当消息被发送给某个对象时,消息函数会根据对象的isa指针指向的类结构在分发表中寻找方法选择器。如果在没找到,objc_msgSend会在超类的分发表中查找。就是这样一层一层往上查询。一旦定位到了选择器,函数就会调用表中的方法并将其传递到接收对象的数据结构中。

这就是runtime中的方法实现,或者说是面向对象的术语:动态绑定。

为了提高消息处理的效率,runtime系统会将调用过的选择器和方法地址进行缓存(就是上面我们提到过的objc_cache)。每个类都有一个独立的cache区域。在分发表中查找之前,消息机制会首先检查接收对象的类的cache区域。如果在cache中能够找到,消息的发送会相对函数调用慢一点。但是app已经运行了足够长的时间,基本上常用的方法都已经缓存起来的话,消息的查找调用效率就杠杠的

隐藏的参数

objc_msgSend找到实现方法的“程序”时,它会调用该“程序”并将消息中所有的参数传递进去。要注意的是,有两个隐藏的参数:

  • 接收者对象
  • 方法选择器

之所以是隐藏的原因,是因为它们并不是在源码而是在方法中进行定义的。所以也就不难解释,为什么objc_msgSend的定义中是这样子的:/* id self, SEL op, ... */ 虽然没有显式声明这些参数,我们仍然能够引用它们。方法将接收者对象引用为self,将接收器作为_cmd。在下面的例子中,_cmd代表了strange方法,self作为接收strange消息的对象

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

获取方法的地址

唯一避免动态绑定的方法,就是直接获取方法的地址并且调用。不过这种获取地址的做法,常见于方法被多次调用的情况,当被大量调用的时候,使用这种做法相对来说会更加高效。 通过NSObjectmethodForSelector:方法,我们可以获取到指向方法实现的指针,并使用它来调用实现。要注意的是,指针在进行类型转换时,需要额外小心。所有返回值和参数类型都需要被包含在转换中。 举个栗子:

void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for (i = 0; i < 1000; i++)
  setter(targetList[i], @selector(setFilled:), YES);

传递给方法实现的前两个参数分别是接收者(self)和方法选择器(_cmd)。这些参数隐藏在方法的语法中,但是在将方法作为函数调用的时候则需要显式传递进来。

使用methodForSelector:来避免动态绑定,能够节省多次发送消息的时间。但是,这种差别只会出现在消息被重复很多很多次的情况下。

最后需要记住一点是methodForSelector:是由Cocoa runtime system提供的,并不属于OC语言的特性。

动态方法解析

除了消息以外,我们也可以提供动态方法来实现函数的调用。比方说,OC中的@dynamic:

@dynamic propertyName;

@dynamic的作用是告知编译器,该属性相关的方法会动态提供。

你可以实现resolveInstanceMethod:resolveClassMethod:两个方法来给实例对象或者类对象动态添加方法的实现。

void dynamicMethodIMP(id self, SEL _cmd) {
  // implementation...
}

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
  if (aSEL == @selector(resolveThisMethodDynamically)) {
    class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "V@:"); // v@:涉及到的是Type Encoding的知识,详细资料可以在Apple文档中查找到
    return YES; // 返回NO的话会触发转发
  }
  
  return [super resolveInstanceMethod:aSEL];
}
@end

类会在转发机制触发之前,可能会有机会来解析这个方法。如果respondsToSelector:或者instancesRespondToSelector:被调用的话,动态方法的解析器首先会给选择器提供IMP。如果你实现了resolveInstanceMethod:方法,但是想要触发转发机制的话,则需要返回NO.

这篇blog中提到一个类方法和实例方法中objc_getClass(self),[self class]objc_getClass([self class])的问题(作者也有结论):

于是就自己研究了一下:

对于类对象:

// RuntimeTest.h
@interface RuntimeTest : NSObject
+ (void)Test2;
@end

// RuntimeTest.m
@implementation RuntimeTest

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(Test2)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(Test)), "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (void)Test {
    NSLog(@"Self: %p", self);
    NSLog(@"[self class]: %p", [self class]);
    NSLog(@"object_getClass(self): %p", object_getClass(self));
    NSLog(@"object_getClass([self class]): %p", object_getClass([self class]));
    NSLog(@"Meta Class: %p", objc_getMetaClass("RuntimeTest"));
}

@end

// 输出
// 2017-04-19 21:51:54.373 ObjectiveCDemo[13405:452834] Self: 0x107d44038
// 2017-04-19 21:51:54.373 ObjectiveCDemo[13405:452834] [self class]: 0x107d44038
// 2017-04-19 21:51:54.373 ObjectiveCDemo[13405:452834] object_getClass(self): 0x107d44010
// 2017-04-19 21:51:54.373 ObjectiveCDemo[13405:452834] object_getClass([self class]): 0x107d44010
// 2017-04-19 21:51:54.374 ObjectiveCDemo[13405:452834] Meta Class: 0x107d44010

可以看到,如果self是一个类对象的话,[self class]指向的是自己本身,objc_getClass(self)objc_getClass([self class])一样,指向的是类对象的元类。因为类方法的定义是在元类中,所以在上面的代码中,我们使用的是object_getClass(self)来获取元类,并在元类中添加方法实现。

而对于实例变量呢?

// RuntimeTest.h
@interface RuntimeTest : NSObject
- (void)varMethod;
@end

// RuntimeTest.m
@implementation RuntimeTest

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(varMethod)) {
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(testInstanceMethod)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)testInstanceMethod {
    NSLog(@"Self: %p", self);
    NSLog(@"[self class]: %p", [self class]);
    NSLog(@"object_getClass(self): %p", object_getClass(self));
    NSLog(@"object_getClass([self class]): %p", object_getClass([self class]));
    NSLog(@"Meta Class: %p", objc_getMetaClass("RuntimeTest"));
}

@end

// 输出
// 2017-04-19 22:01:36.326 ObjectiveCDemo[13541:460804] Self: 0x608000008230
// 2017-04-19 22:01:36.326 ObjectiveCDemo[13541:460804] [self class]: 0x102390068
// 2017-04-19 22:01:36.326 ObjectiveCDemo[13541:460804] object_getClass(self): 0x102390068
// 2017-04-19 22:01:36.326 ObjectiveCDemo[13541:460804] object_getClass([self class]): 0x102390040
// 2017-04-19 22:01:36.326 ObjectiveCDemo[13541:460804] Meta Class: 0x102390040

可以看到,这里[self class]object_getClass(self)一样指向类对象,object_getClass([self class])指向的是元类。因为实例方法是定义在类对象中,所以这里使用的是[self class]


结论是:

1. 如果对象是类对象的话,self[self class]指向的是类对象自身,object_getClass(self)object_getClass([self class])指向的是元类。在进行动态方法解析是,需要使用的是object_getClass(self), 即在元类上添加类方法的实现。因为类方法的定义是在元类上 2. 如果对象是实例对象的话,self指向的是对象本身,[self class]object_getClass(self)指向的是类对象,object_getClass([self class])指向的是元类对象。在进行动态方法解析时,需要使用[self class],即在类上添加实例方法的实现。因为实例方法是定义在类对象上的。 3. 如果忘了用哪个的话。。用object_getClass(self)就是最妥当的。。简单来记的话,就是:self是自身,object_getClass([self classs])是元类,objcect_getClass(self)是上级。[self class]实例时为类,类时为自身。

2017-04-21 update: 4.object_getClass(self)获取的是,isa的指向 5.注意isKindOfClassisMemberOfClass

BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
  BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
  BOOL res3 = [(id)[Person class] isKindOfClass:[Person class]];
  BOOL res4 = [(id)[Person class] isMemberOfClass:[Person class]];

上面的代码中,只有res1为YES,其他的都为NO,为什么呢?

+ (BOOL)isMemberOfClass:(Class)cls {
  return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
  return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
  for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
      if (tcls == cls) return YES;
  }
  return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
  for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
      if (tcls == cls) return YES;
  }
  return NO;
}

isKindOfClass:isMemberOfClass:的定义中,我们可以看到,当对象为类对象时,用来比对的是元类对象和类对象,对象为实例对象时,比对的是类对象!

第一句代码使用的是isKindOfClass:,比较的是[NSObject class]object_getClass([NSObject class])获取的是NSObject这个类对象的元类,该元类的超类指向了NSObject这个类对象,所以返回YES。而第二句代码,比较的是NSObject这个类对象和其元类对象,所以返回NO。

第三句代码比较的是Person元类的结构和Person类对象,返回NO 第四句代码比较的是Person元类和Person这个类,故返回NO

6.总结

对象类型 self [self class] object_getClass(self) object_getClass([self class])
实例对象 自身 类对象 类对象 元类
类对象 自身 自身 元类 根元类

方法转发

当将消息发送给一个“不认识”它的对象时,会发生什么呢?

  1. 动态方法解析: resolveInstanceMethod:resolveClassMethod:返回YES的话,表明能处理这个消息了,返回NO的话,进行第二步;
  2. 重定向(也有叫Fast Forwarding): forwardingTargetForSelector:将消息转发给另一个对象。如果这个方法返回nil或者self的话,就会将消息发送给另一个对象进行处理
  3. Normal Forwarding: forwardInvocation:&methodSignatureForSelector:
  4. 如果上面三个步骤都不满足的话,就会发送doesNotRecognizeSelector:消息

Fast Forwarding

forwardingTargetForSelector:返回nil或者self的话,则会进行Normal Forwarding,继续消息转发。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(myMethod)) {
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(myMethod)) {
        return NSClassFromString(@"AlternateClass");
    }
    return [super forwardingTargetForSelector:aSelector];
}

Forwarding

向不处理该消息的对象发送消息是错误的做法。但是在报错之前,runtime系统会给接收对象机会来处理这个消息。在报错之前,runtime会发送一个forwardInvocation:消息,在消息里,将NSInvocation对象(该对象封装了原来的消息和参数)作为参数一并发送给该对象。

因此,我们可以实现forwardInvocation:方法来给出个缺省的响应来避免报错。正如方法名一样,forwardInvocation:做的事情,就是将消息转发给另一个对象。

想象下,你声明了一个可以响应negotiate消息的对象,但是你同时该对象的响应里面,也能包含有来自其他类型对象的响应,这时候你会怎么做咧?很简单,你可以在negotiate的消息的方法实现中,转发给其他对象即可。

更进一步的说,你希望对于negotiate这个消息的响应由其他类来实现,但是又不想通过继承来做的话,也可以将消息转发给能实现的类来完成:

- (id)negotiate {
  if ([someOtherObject respondsTo:@selector(negotiate)]) {
    return [someOtherObject negotiate];
  }
  return self;
}

要转发消息的话,在forwardInvocation:的方法中需要决定消息的去处以及转发出去的时候,需要带上原来的参数。可以通过invokeWithTarget:来进行转发

- (void)forwardInvocation:(NSInvocation *)anInvocation {
  if ([someOtherObject respondsToSelector:[anInvocation selector]]) {
    [anInvocation invokeWithTarget:someOtherObject];
  } else {
    [super forwardInvocation:anInvocation];
  }
}

说了辣么多,anInvocation是哪里来的?在本节开头的第三个步骤里面,不知道你是否注意到了methodSignatureForSelector:这个方法。methodSignatureForSelector:forwardInvocation一般是配套使用的。在调用forwardInvocation:之前, runtime系统会向对象发送methodSignatureForSelector:来获取方法签名用于生成NSInvocation对象。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    NSMethodSignature *signature = [super methodSignatureForSelector:selector];
    if (!signature) {
        signature = [self methodSignatureForSelector:selector];
    }
    return signature;
}

我们可以将forwardInvocation:方法看做是那些“不认识”方法的分配中心,将这些分发分配给不同的接收者,或者将所有消息一股脑发送到同一个目的地(跟收发快递的机制差不多嘛~😁)。forwardInocation:方法同时也能将几个不同的消息合并到同一个响应里面。 **注意: **forwardInvocation:只在对象不能识别消息的时候会执行!

转发和多继承

大家都知道,OC是不允许多继承的,但是,转发机制却模仿了继承的机制。通过转发,我们能实现类似多继承的效果,如下图所示,通过消息转发将其他类的方法继承(“借”)过来用:

Forwarding

在上面的图中,Warrior的实例将negotiate消息转发给Diplomat类的实例。对Warrior来说,negotiate的响应就是Diplomat类实例给出的响应,看起来就像Warrior给出的响应一样。

因此,转发消息的对象看起来就像继承了另一个类一样。在上面的例子中,给人的感觉就是Warrior类继承了Diolimat类。

转发机制提供了另外一些特性,你可以把这些特性作为多继承来使用。转发和多继承有个重要的不同点就是:多继承将不同功能合并在一个对象想中,它可能会变得臃肿,变得多层面。而转发,是将不同的工作分发给不同的对象。它将问题化小分给众多对象,且这些对象对于消息发送者来说是透明不可见的。

也就是说,多继承是一个对象有N个功能,转发呢,就是将问题交给别的对象去完成。

替代者对象

转发不仅仅与多继承相似,它还能让轻量级的对象来替代重量级的对象。

OC中的proxy(代理)就是一种替代方式。代理对象会关注转发给远端接收者的消息的细节,确保所有的参数都被copy并且能够通过连接进行取回等。但是它并不会执行远端对象的功能,仅仅给它一个本地地址,让它能够接收消息而已。

转发和继承

虽然转发和继承有点相似,但是NSObject从不会将两者弄混。respondsToSelector:isKindOfClass:会在继承结构中查找对应的方法,而不会在转发链中。

if ([aWarrior respondsToSelector:@selector(negotiate)]) {
  ...
}

上面的respondsToSelector:的结果是NO。虽然它可以接收消息,并且能够进行消息响应,但是实际上,消息是被转发给Diplomat对象来实现的。

在大多数情况下,NO才是正确的答案。不过,如果你使用转发来设置一个替代者或者扩展了该类的话,转发机制可能就会像继承一样。如果你想要你的对象看起来像继承一样的话,你需要重写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;
}

除了respondsToSelector:isKindOfClass:以外,instancesRespondsToSelector:方法同样也应该实现转发算法。如果使用了协议的话,那么conformsToProtocol:方法也要重写了。同样的,如果对象转发了消息的话,则需要通过methodSignatureForSelector:来返回准确的方法描述,这个方法最终会响应被转发的信息。比方说,假如一个对象能够将消息转发给替代者,你可以这样做:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

参考链接:

  1. Objective-C Runtime
  2. Runtime Programming Guide