Objective-C 的消息转发机制

通常,向一个对象发送它不能处理的消息会得到类似这样的错误提示:No visible @interface for 'SomeObject' declares the selector 'someSelector:'instance method '-someMethod' not found 等等,而动态语言,如 Ruby,则有另一种处理方式:

class MyClass
  def initialize
    @items = []
  end
  def method_missing(m, *args, &block)
    unless @items.respond_to? m
      super(m, *args)
    end
    @items.__send__(m, *args)
  end
end
t = MyClass.new
t.push "hello"
puts t.first # output: "hello"
t.trim       # error: undefined method `trim' (NoMethodError)

这种特性赋予了开发者很大的自由性和便利性。

虽然 Objective-C 并不是动态语言,但却具备动态类型,有类似的特性 -- 消息转发

快速转发

这种方式把消息原封不动地转发给目标对象,速度是最快的也是最简单的 -- 仅需在类里实现 - (id)forwardingTargetForSelector:(SEL)aSelector;

@interface NewArray : NSObject
@property (strong, nonatomic) NSArray *array;
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (id)firstObject;
- (NSUInteger)count;
@end

@implementation NewArray
- (id)forwardingTargetForSelector:(SEL)aSelector {
  return [_array respondsToSelector:aSelector] ?
          _array : nil;
}
- (id)firstObject {
  return _array.count ? [_array objectAtIndex:0] : nil;
}
- (NSUInteger)count { return 0; }
@end

NewArray *arr = [[NewArray alloc] init];
arr.array = @[@"a", @"b"];
NSLog(@"%@ and %@, count %li", [(NSArray *)arr lastObject],
      [arr firstObject], [arr count]);
// output: "b and a, count 0"

普通转发

这种方式让你可以控制目标对象、方法和参数,但是性能没有第一种方法好。这个方法需要实现 - (void)forwardInvocation:(NSInvocation *)anInvocation;- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector;

@implementation NSArray (MessageForwarding)
- (void)forwardInvocation:(NSInvocation *)anInvocation {
  for (id obj in self) {
    // Only apply for string that lenght == 1
    if (1 == [(NSString *)obj length]) {
      [anInvocation invokeWithTarget:obj];
    }
  }
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  NSMethodSignature *signature = [super methodSignatureForSelector:sel];
  if (!signature) {
    for (id obj in self) {
      if ((signature = [obj methodSignatureForSelector:aSelector])) {
        break;
      }
    }
  }
  return signature;
}
@end

NSArray *arr = @[[NSMutableString stringWithString:@"a"],
                 [NSMutableString stringWithString:@"bc"]];
[(NSMutableString *)arr appendString:@"!"];
NSLog(@"%@", arr);
// output: "a! bc"

-methodSignatureForSelector: 的作用是告诉运行时参数有多少个、是什么类型,这是用来兼容 C 的。

转发的注意事项

可能你已经在上述代码里发现了一些问题,这是它们的共同问题:

  1. 发送非自己处理的消息时需要转换类型,如 [(NSArray *)arr lastObject];
  2. 如果第一个接收消息的对象实现了同样的方法,则不会触发转发,如第一例中的 [arr count]

第一个问题,目的是让运行时知道要找什么类型,以第一例为例,如果改成 [(NSString *)arr lastObject] 则会触发 Xcode 报错,CodeRunner 则只会警告。一般来说,用目标类型即可,比如 NSArray 就用 NSArray,不过我比较喜欢动态语言的方式,所以一般用 id

第二个问题,需要用别的方式来解决,NSProxy

NSProxy

使用 NSProxy 需要实现一个子类。

#import <Foundation/Foundation.h>
@interface ArrayProxy : NSProxy
@property (strong, nonatomic) NSArray *items;
+ (id)proxyWithTarget:(NSArray *)anArray;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
@end

@implementation ArrayProxy
+ (id)proxyWithTarget:(NSArray *)anArray {
  ArrayProxy *proxy = [ArrayProxy alloc];
  proxy.items = anArray;
  return proxy;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
  for (id obj in _items) {
    [anInvocation invokeWithTarget:obj];
  }
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  // DO NOT do this:
  // NSMethodSignature *signature = [super methodSignatureForSelector:sel];
  // Will raise `NSInvalidArgumentException`
  NSMethodSignature *signature;
  for (id obj in _items) {
    if ((signature = [obj methodSignatureForSelector:aSelector])) {
      break;
    }
  }
  return signature;
}
@end

@implementation NSArray (MessageForwarding)
- (id)do { return [ArrayProxy proxyWithTarget:self]; }
@end

@implementation NSString (MessageForwarding)
- (NSUInteger)count {
  NSLog(@"%li", self.length);
  return self.length;
}
@end

NSArray *arr = @[@"a", @"bc"];
[[arr do] count];
// output: 1, 2

因为是经过 NSProxy 来处理,所以就不受对象是否能响应对应消息的限制。另,不要在 -methodSignatureForSelector: 里往 super 发送消息,会抛出 NSInvalidArgumentException

需要注意的是,-do 只返回最后一个结果:

NSLog(@"%li", [[arr do] length]); // 2

结尾

上面的示例代码仅供参考如何实现消息转发,除此以外的部分有更好的实现方式,比如 firstObject 就应该用 category 来实现,而有些需求,则可以使用 delegate,比如 UI 元素。

我对上面三种消息转发的主要用法是扩展无法通过 category 实现的功能,比如为 NSMutableArray 添加可操作自身的方法,Objective-C 不允许操作自身,所以像 while(obj = arr.unshift()) 之类的技巧就无法使用。