在对象上调用方法是Objective-C中常使用的功能,用OC的术语来说,叫“传递消息”(pass a message)。消息有“名称”(name)或“选择子”(selector),可以接收参数,而且可能还有返回值。
C语言使用的是“静态绑定”(static binding),即在编译期就能决定运行时所应调用的函数。
OC使用的是“动态绑定”(dynamic binding),所要调用的函数直到运行时才能确定。给对象发送消息可以这样写:
id returnValue = [someObject messageName:parameter];
其中someObject
叫做“接受者”(receiver),messageName
叫做“选择子”(selector)。选择子与参数合起来称为“消息”(message)。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数:objc_msgSend
,其“原型”(prototype)如下:
// 返回值类型; 参数:接受者、选择子(SEL是选择子的类型)、n个参数
void objc_msgSend(id self, SEL cmd, ...)
这是个“参数个数可变的函数”(variadic function),编译器会把刚才的例子转换如下:
id returnValue = objc_msgSend(someObject,
@selector(messageName:),
parameter);
objc_msgSend函数会依据接收这与选择子的类型来调用适当的方法。查找顺序如下:
在接受者所属类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。
若找不到,则沿着类的继承体系
继续向上查找,等找到合适的方法之后再跳转。OC中的继承体系
如下:
。
类向上找至根类,根类再向上是元类。
若最终还是没找到相符的方法,那就就会执行“消息转发”(message forwarding)操作。
这么看来,想调用一个方法似乎需要很多步骤。所幸objc_msgSend
会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若稍候还向该类发送与选择子相同的消息,那么执行起来就很快了。
消息转发机制
流程图如下:
。
系统给了三次补救的机会。
对象/类 在接收到无法解读的消息后,首先将调用下列类方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector; // 对象无法解读
+ (BOOL)resolveClassMethod:(SEL)selector; // 类的无法解读
该方法参数接收了无法响应的那个方法的选择子,返回值类型为BOOL:表示这个类能否新增一个实例方法用以处理该选择子。
1. 若此步骤的returnValue为NO,这会进入下一步(备援接收者)。
2. 若想成功响应的前提是:相关方法的实现代码已经写好,只等运行的时候动态插在类里。此方案常用来实现@dynamic
属性。例如:
void autoDictionarySetter(id self, SEL _cmd, id value);
id autoDictionaryGetter(id self, SEL _cmd);
+ (BOOL)resolveInstanceMethod:(SEL)selector {
NSString *selectorString = NSStringFromSelector(selector);
if (/* selector is from a @dynamic property */) {
if ([selectorString hasPrefix:@"set"]) {
class_addMethod(self,
selector,
(IMP)autoDictionarySetter,
"v@:@");
} else {
class_addMethod(self,
selector,
(IMP)autoDictionarySetter,
"v@:@");
}
return YES;
}
return [super resolveInstanceMethod:selector];
}
我们也可以吞噬无法响应的选择子,为此方法添加log,方便debug哪个方法没有实现:
/**
要动态绑定的方法
@param self 要绑定方法的对象
@param _cmd 方法信息
@param value 方法参数
*/
void dynamicMethodIMP(id self, SEL _cmd, id value) {
NSString *sel = NSStringFromSelector(_cmd);
NSLog(@"self = %@ _cmd = %@ value = %@", self, sel, value);
}
// 1. Method resolution:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// v表示返回类型是void、@表示id、:表示SEL、@表示方法的具体参数
class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:@");
return [super resolveInstanceMethod:sel]; // 返回YES, 整个消息发送过程会重启
}
+ (BOOL)resolveClassMethod:(SEL)sel {
class_addMethod(self.class, sel, (IMP)dynamicMethodIMP, "v@:@");
return [super resolveClassMethod:sel]; // 返回YES, 整个消息发送过程会重启
}
第二步会调用如下方法,当此方法返回备援接受者
(不是self或nil)时,重启整个发送过程。
- (id)forwardingTargetForSelector:(SEL)selector;
可以利用此步骤通过“组合”(composition)的方式模拟出“多重继承”(multiple inheritance)的某些特性。在一个对象的内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,就好像是该对象亲自处理了这些消息似的。举了个实现此步骤的例子如下:
// 2. Fast forwarding: 可以把消息转发给其他对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString *method = NSStringFromSelector(aSelector);
if ([method isEqualToString:@"eating"]) {
Dog *dog = [[Dog alloc] init];
return dog;
}
return nil; // 返回的不是 nil or self, 整个消息发送过程会重启, 当然发送的对象会变成return的对象
}
如果此步骤的返回nil,这会进入下一步。
如果转发算法来到这一步的话,唯一能做的就是启用完整的消息转发机制了。会触发以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
doesNotRecognizeSelector:
方法,导致crash:Unrecognized selector to XXX
。- (void)doesNotRecognizeSelector:(SEL)aSelector;
NSInvocation
对象,把尚未处理的那条消息有关的全部细节都封于其中,此对象包含选择子、目标(target)及参数。然后触发如下方法,其参数就是此对象:- (void)forwardInvocation:(NSInvocation *)anInvocation;
这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。举例实现如下:
// 3. Normal forwarding: 会创建 NSInvocation 对象,开销较大
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSString *methodName = NSStringFromSelector(aSelector);
if ([methodName isEqualToString:@"eating"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
Dog *dog = [[Dog alloc] init];
if ([dog respondsToSelector:sel]) {
[anInvocation invokeWithTarget:dog];
return;
}
[super forwardInvocation:anInvocation];
}
然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少人采用这么简单的实现方式。比较有用的实现方式:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子,等等。
参考:《Effective Objective-C 2.0》