Objective-C 消息转发

Mike Ash Friday Q&A 中文译文:Objective-C 消息转发

作者 TommyWu
封面圖片: Objective-C 消息转发

译文 · 原文: Friday Q&A 2009-03-27: Objective-C Message Forwarding · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2009-03-27-objective-c-message-forwarding.html 发布:2009-03-27 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


欢迎回到又一个精彩的 Friday Q & A 问答环节。本周我将继续深入探讨 Objective-C 运行时(Objective-C runtime)的系列文章。Yuji Tachikawa 建议谈谈 @dynamic 属性在 CoreData 中的工作原理,我打算以此为出发点,扩展讨论一般性的消息转发(message forwarding)机制。

无此方法 上周我介绍了 Objective-C 的消息发送(message sending)工作原理,并提到当某个给定的选择子(selector)找不到方法实现(method implementation)时,会发生一些有趣的事情。正是这些有趣的事情促成了转发的发生。

(如果你对选择子是什么、方法和消息之间的区别还不太清楚,你可能想快速阅读一下那篇文章,或者如果你已经读过,至少重新阅读一下定义部分。)

那么,究竟什么是消息转发?简单来说,它允许捕获并处理未知的消息。换言之,任何时候当一个未知的消息被发送时,它会以一个封装好的形式传递给你的代码,这时你可以对它进行任何你想要的操作。

这种能力极其强大,能够实现各种巧妙而聪明的事情。

当你执行 [foo bar]foo 并未实现 bar 方法时,会发生什么?如果它实现了该方法,过程就很简单:运行时会查找对应的方法实现(method implementation),然后跳转执行。但当找不到这样的方法时,一连串复杂的事件就会接踵而至:

延迟方法解析(Lazy method resolution)。这是通过向相关类发送 resolveInstanceMethod:(对于类方法则是 resolveClassMethod:)来完成的。如果该方法返回 YES,则消息发送(message sending)会重新启动,并假设对应的方法此时已被添加。

快速转发路径(Fast forwarding path)。如果目标对象实现了 forwardingTargetForSelector: 方法,就会将此消息发送给它。若该方法实现后返回的不是 nilself,则整个消息发送过程会以该返回值作为新目标重新启动。

常规转发路径(Normal forwarding path)。首先,运行时会发送 methodSignatureForSelector: 来查看存在何种类型的参数和返回类型。如果返回了一个方法签名(method signature),运行时就会创建一个描述当前所发送消息的 NSInvocation 对象,然后将 forwardInvocation: 消息发送给该对象。如果未找到方法签名,运行时就会发送 doesNotRecognizeSelector:

延迟解析
如我们上周所了解的,运行时通过查找方法实现(method implementation,即 IMP)并跳转执行来发送消息。有时动态地将 IMP 注入类中而非预先全部设置完毕,会很有用。这样做能实现极快的「转发」(forwarding),因为方法一旦被解析,就会作为正常消息发送流程的一部分被调用。当然,缺点在于这种方法不够灵活 —— 你需要准备好待注入的 IMP,而这又意味着你必须提前预知将要传入的参数和返回类型。

这种机制非常适合诸如 @dynamic 属性(@dynamic properties)之类的场景。方法签名(method signature)是你应当提前知道的:要么接收一个参数且返回值为 void,要么无参数并返回一个值。值的类型可能各异,但你可以覆盖常见情况。由于 IMP 会接收到发送给对象的选择子(selector),它就能利用该选择子动态获取属性名称并查找对应值。通过 +resolveInstanceMethod: 将其注入类中即可完成设置。

快速转发(Fast Forwarding)
接下来,运行时会检查你是否希望将消息原封不动地转发给另一个对象。由于这是转发的常见场景,这种机制能够以最小开销实现它。

出于某种原因,快速转发的文档记录非常匮乏。除了 NSObject.h 中被注释掉的声明外,苹果仅在 Leopard 版本的发布说明中提及过此特性(可搜索 “New forwarding fast path”)。

这项技术非常适合模拟多重继承。你可以编写如下覆盖方法:

- (id)forwardingTargetForSelector:(SEL)sel { return _otherObject; }

正常转发

前两种基本上只是优化,可以让转发更快。如果你没有利用它们,完整的转发机制就会介入。这会创建一个 NSInvocation 对象,完全封装正在发送的消息。它包含目标、选择子(selector)以及所有参数,也允许完全控制返回值。

在运行时(runtime)能够构建 NSInvocation 之前,它需要一个 NSMethodSignature,因此会通过 -methodSignatureForSelector: 来请求。这是由于 Objective-C 的 C 语言渊源所必需的。为了将参数打包进 NSInvocation 中,运行时需要知道参数的类型以及数量。这些信息通常在 C 运行时环境中是不提供的,因此它必须绕过 C 语言的 “字节集合” 世界观,通过另一种方式来获取类型信息。

一旦调用对象(invocation)构造完成,运行时就会调用你的 forwardInvocation: 方法。从那里开始,你可以随意处理它传递给你的调用对象。可能性是无穷无尽的。

这里有一个简单的例子。想象一下你已经厌倦了编写循环,因此希望能够更直接地操作数组。将这个小小的 category(分类)添加到 NSArray 中:

@implementation NSArray (ForwardingIteration)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
NSMethodSignature *sig = [super methodSignatureForSelector:sel];
if(!sig)
{
for(id obj in self)
if((sig = [obj methodSignatureForSelector:sel]))
break;
}
return sig;
}
- (void)forwardInvocation:(NSInvocation *)inv
{
for(id obj in self)
[inv invokeWithTarget:obj];
}
@end
[(NSWindow *)windowsArray setHidesOnDeactivate:YES];

NSProxy 本质上是一个专为代理功能(proxying)设计的类。它只实现了最少量的基础方法,把其他所有功能都留给了子类自由发挥。这意味着一个实现了消息转发(forwarding)的子类,基本上可以捕获任何消息。

要使用 NSProxy 实现这类功能,你需要编写一个 NSProxy 子类,让它可以被初始化为指向某个数组,然后在 NSArray 上添加一个简单的桩方法(stub method)来返回该代理类的新实例,如下所示:

@implementation NSArray (ForwardingIteration)
- (id)do { return [MyArrayProxy proxyWithArray:self]; }
@end
[[windowsArray do] setHidesOnDeactivate:YES];

声明 Objective-C 继承自 C 的另一个后果是,编译器需要知道你代码中每条将要发送的消息的完整方法签名(method signature),即便是纯粹进行转发的消息也不例外。举个构造性的例子,假设你正在编写一个通过转发从代码中生成整数的类,这样你就可以这样写:

int x = [converter convert_42];

问题在于编译器不认识任何名为 convert_42 的方法,因此无法知道它会返回何种类型的值。这会导致编译器产生严重警告,并默认假设该方法返回 id 类型。解决办法很简单,只需在某处声明该方法原型即可:

@interface NSObject (Conversion)
- (int)convert_42;
- (int)convert_29;
@end

结论
消息转发(message forwarding)是一项强大的技术,它极大地增强了 Objective-C 的表达能力。Cocoa 将其应用于 NSUndoManager 和分布式对象(distributed objects)等场景,同时也能让你在自己的代码中实现许多巧妙的功能。

本周的 Friday Q & A 到此结束。下周我们将继续带来更多引人入胜的编程故事,欢迎在本文下方发表评论。

Friday Q & A 的内容源于读者的创意贡献。如果你有希望在此讨论的想法,请在评论区提出或直接发邮件给我。除非你特别要求匿名,否则我会注明你的姓名。


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2009-03-27-objective-c-message-forwarding.html

Welcome back to another exciting Friday Q&A. This week I’m going to continue the series on the Objective-C runtime. Yuji Tachikawa suggested talking about how @dynamic properties work in CoreData and I’m going to take that and expand it to talk about message forwarding in general.

No Such Method Last week I talked about how Objective-C messaging works, and mentioned that interesting things happen when no method is found for a given selector. Those interesting things are what make forwarding happen.

(If you aren’t totally clear on what a selector is or what the difference is between a method and a message, you might want to go read through that article real quick, or at least re-read the Definitions section if you already read it.)

Just what is message forwarding? Simply speaking, it allows unknown messages to be trapped and reacted to. In other words, any time an unknown message is sent, it gets delivered to your code in a nice package, at which point you can do whatever you like with it.

This kind of thing is incredibly powerful and allows for doing all kinds of nifty, clever things.

What Happens What happens when you do [foo bar] and foo doesn’t implement a bar method? When it does implement such a method, it’s pretty straightforward: it looks up the appropriate method, then jumps to it. When no such method can be found, a complicated sequence of events ensues:

  • Lazy method resolution. This is done by sending resolveInstanceMethod: (resolveClassMethod: for class methods) to the class in question. If that method returns YES, the message send is restarted under the assumption that the appropriate method has now been added.

  • Fast forwarding path. This is done by sending forwardingTargetForSelector: to the target, if it implements it. If it implements this method and it returns something other than nil or self, the whole message sending process is restarted with that return value as the new target.

  • Normal forwarding path. First the runtime will send methodSignatureForSelector: to see what kind of argument and return types are present. If a method signature is returned, the runtime creates an NSInvocation describing the message being sent and then sends forwardInvocation: to the object. If no method signature is found, the runtime sends doesNotRecognizeSelector:.

Lazy Resolution As we learned last week, the runtime sends messages by looking up a method, or IMP, and then jumping to it. Sometimes it can be useful to dynamically plug IMPs into a class instead of setting them all up beforehand. Doing this allows for really fast “forwarding”, because after the method is resolved, it gets invoked as part of the normal message sending process. The disadvantage is, of course, that this isn’t very flexible, since you need to have an IMP ready to plug in, and that in turn means that you need to have already anticipated the argument and return types that will be arriving.

This kind of thing is great for stuff like @dynamic properties. The method signature is something you should know in advance: you’ll either take one parameter with a void return, or have no parameters and return one value. The types of the values will vary, but you can cover the common cases. Since the IMP gets passed the selector that’s been sent to the object, it can use that selector to get the name of the property and look it up dynamically. Plug it in to the class using +resolveInstanceMethod: and off you go.

Fast Forwarding The next thing the runtime does is see if you want to just send the whole message unchanged to a different object. Since this is a common case of forwarding, this allows it to be done with minimal overhead.

For some reason, fast forwarding is really poorly documented. The only place Apple even mentions it, aside from a commented-out declaration in NSObject.h, is in the Leopard release notes. (Search for “New forwarding fast path”.)

This technique is great for faking multiple inheritence. You can write a little override like this:

- (id)forwardingTargetForSelector:(SEL)sel { return _otherObject; }

Normal Forwarding The first two are basically just optimizations that allow forwarding to go faster. If you don’t take advantage of them, the full forwarding mechanism goes into action. This creates an NSInvocation object which fully encapsulates the message being sent. It holds the target, the selector, and all of the arguments. It also allows full control over the return value.

Before the runtime can build the NSInvocation it needs an NSMethodSignature, so it requests one using -methodSignatureForSelector:. This is required due to Objective-C’s C heritage. In order to bundle the arguments up in the NSInvocation, the runtime needs to know what kind of arguments there are, and how many of them there are. This information isn’t normally provided in a C runtime environment, so it has to do an end run around the C “bag of bytes” view of the world and get that type information in another way.

Once the invocation is constructed, the runtime then invokes your forwardInvocation: method. From there you can do whatever you want with the invocation it hands you. The possibilities are endless.

Here’s one quick example. Imagine you’re tired of writing loops, so you want to be able to manipulate arrays more directly. Add this little category to NSArray:

@implementation NSArray (ForwardingIteration)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
NSMethodSignature *sig = [super methodSignatureForSelector:sel];
if(!sig)
{
for(id obj in self)
if((sig = [obj methodSignatureForSelector:sel]))
break;
}
return sig;
}
- (void)forwardInvocation:(NSInvocation *)inv
{
for(id obj in self)
[inv invokeWithTarget:obj];
}
@end
[(NSWindow *)windowsArray setHidesOnDeactivate:YES];

NSProxy is basically a class that’s expilicitly designed for proxying. It implements a minimal subset of methods, leaving everything else up for grabs. This means that a subclass that implements forwarding can capture basically any message.

To use NSProxy for this kind of thing, you’d write an NSProxy subclass that can be initialized to point at an array, and then add a little stub method to NSArray that returns a new instance of the proxy, like so:

@implementation NSArray (ForwardingIteration)
- (id)do { return [MyArrayProxy proxyWithArray:self]; }
@end
[[windowsArray do] setHidesOnDeactivate:YES];

Declarations Another consequence of Objective-C’s C heritage is that the compiler needs to know the full method signature of every message that you’re going to send in your code, even purely forwarded ones. To make a contrived example, imagine writing a class that uses forwarding to produce integers from code, so that you can write this:

int x = [converter convert_42];

The trouble is that the compiler doesn’t know about any convert_42 method, so it has no idea what kind of value it returns. It will give you a nasty warning, and will assume that it returns id. The fix to this is simple, just declare one somewhere:

@interface NSObject (Conversion)
- (int)convert_42;
- (int)convert_29;
@end

Conclusion Message forwarding is a powerful technique that greatly multiplies the expressiveness of Objective-C. Cocoa uses it for things like NSUndoManager and distributed objects, and it can let you do a lot of nifty things in your own code.

That wraps up this week’s Friday Q&A. Tune in next week for more riveting tales of programming, and leave your comments on this edition below.

Friday Q&A is powered by the contribution of your ideas. If you have an idea you’d like to see discussed here, post it in the comments or e-mail it directly. I will use your name unless you ask me not to.