What are the stages of message forwarding, and when would you use them?

4.1.3 · L5 · Learning

Predict first

What does this print?

class Proxy: NSObject {
    let target = NSString(string: "hello")
    override func forwardingTarget(for s: Selector!) -> Any? { target }
}
print(Proxy().perform(NSSelectorFromString("uppercaseString"))?.takeUnretainedValue() as Any)
  • nil
  • Optional(HELLO)
  • Crash

Predict first

In which order does the runtime try these?

// 1) forwardInvocation:
// 2) resolveInstanceMethod:
// 3) forwardingTarget(for:)
  • 2, 3, 1
  • 1, 2, 3
  • 3, 2, 1

Mental model. A receptionist who cannot answer a call first checks for a stored answer, then transfers the call, then takes a full message, and only then says “wrong number”.

When a selector is not found, the runtime gives the object several chances before it throws. First, resolveInstanceMethod: may add the method dynamically (how @dynamic properties are backed). Then forwardingTarget(for:) may return another object to receive the message, which is a cheap, fast redirection. Next, the heavyweight stage: methodSignature(for:) and forwardInvocation(_:), which receive the whole message as an NSInvocation that you can inspect, modify, replay or send to several targets. Only if all of them decline does doesNotRecognizeSelector: raise the exception.

These stages power proxies (NSProxy), decorators that forward everything except a few methods, undo managers, and Core Data’s dynamic accessors. They also explain odd stack traces. In new Swift code you rarely write them, because protocols and composition do the same job with compile-time checking.

Proxy has no uppercaseString, so the runtime asks forwardingTarget, and the string it returns handles the message.

First try to add the method, then redirect cheaply to another object, and only then build a full invocation.

Common traps

  • Using forwardInvocation when forwardingTarget is enough. It is much slower, because it builds an invocation object.
  • Forgetting that respondsToSelector: does not know about forwarding. A forwarded selector can still return false unless you override it.

They might follow up with

  • What is NSProxy? A root class that does nothing but forward messages, for building transparent proxies.
  • Do Swift classes get this? Only NSObject subclasses, and only for @objc selectors sent through the runtime.

Related questions

  • 4.1.1 What happens when you send a message in Objective-C, and what if nothing responds?
  • 4.6.1 How does method swizzling work, and why does it need dynamic in Swift?

← What is a selector, how does #selector map Swift names to it, and…   What is isa, and why does your object’s runtime class sometimes… →