Dynamic Method Resolution and Forwarding
|
This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
When objc_msgSend searches a class and its superclasses and finds no implementation for a selector, it does
not fail immediately. The runtime gives the receiver three consecutive chances to handle the message
anyway. Only if all three decline does the familiar unrecognized selector sent to instance exception appear.
These hooks are how Core Data supplies accessors it never compiled, how mocking frameworks stand in for real objects, and how a wrapper can transparently behave like the thing it wraps.
The Forwarding Chain
or anywhere up the superclass chain"] --> B["Step 1 — Dynamic resolution
+resolveInstanceMethod: (or +resolveClassMethod:)"] B -->|"returns YES after
class_addMethod"| B2["Dispatch restarts from scratch.
The new method now handles it."] B -->|returns NO| C["Step 2 — Fast forwarding
-forwardingTargetForSelector:"] C -->|"returns another object"| C2["objc_msgSend is re-sent to that object.
Cheap: no NSInvocation is built."] C -->|"returns nil or self"| D["Step 3 — Normal forwarding
-methodSignatureForSelector:"] D -->|"returns nil"| F D -->|"returns a signature"| E["An NSInvocation is built, then
-forwardInvocation: is called"] E -->|"you invoke it on a target,
or set a return value"| E2["Message handled.
Any number of targets possible."] E -->|"not handled"| F["-doesNotRecognizeSelector:"] F --> G["NSInvalidArgumentException:
unrecognized selector sent to instance"] style B2 fill:#dbe9d5,stroke:#4a7a3a style C2 fill:#dbe9d5,stroke:#4a7a3a style E2 fill:#dbe9d5,stroke:#4a7a3a style G fill:#e05252,stroke:#8a1f1f,color:#fff
The three steps are ordered by cost: resolution is a one-off fix, fast forwarding is a single extra message
send, and normal forwarding builds an NSInvocation object and is therefore the slowest. Implement the
earliest one that does the job.
The lookup phase that precedes all this is described in Messaging and Selectors.
Step 1: Dynamic Method Resolution
+resolveInstanceMethod: is the runtime asking "can you provide this method right now?". Add it with
class_addMethod and return YES; dispatch then starts over and finds it.
#import <objc/runtime.h>
// A plain C function with the (self, _cmd, ...) shape of an IMP.
static void dynamicSetter(id self, SEL _cmd, id value) {
NSString *key = [self keyForSelector:_cmd];
[[self backingStore] setObject:value forKey:key];
}
static id dynamicGetter(id self, SEL _cmd) {
NSString *key = NSStringFromSelector(_cmd);
return [[self backingStore] objectForKey:key];
}
@implementation DynamicRecord
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *name = NSStringFromSelector(sel);
if ([name hasPrefix:@"set"] && [name hasSuffix:@":"]) {
class_addMethod(self, sel, (IMP)dynamicSetter, "v@:@"); // void, self, _cmd, id
return YES;
}
if ([self isKnownPropertyName:name]) {
class_addMethod(self, sel, (IMP)dynamicGetter, "@@:"); // id, self, _cmd
return YES;
}
return [super resolveInstanceMethod:sel]; // always give super a chance
}
@end
Because the method is genuinely added to the class, the cost is paid once: every later send hits the
normal cache. resolveClassMethod:` does the same for ` methods (add them to the metaclass, obtained with
object_getClass(self)).
@dynamic Properties
This is the mechanism behind @dynamic. Declaring a property @dynamic tells the compiler not to synthesise
anything and to stop warning — you are promising the accessors will exist at run time:
@interface DynamicRecord : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSDate *createdAt;
@end
@implementation DynamicRecord
@dynamic title, createdAt; // resolved by +resolveInstanceMethod: above
@end
Core Data’s NSManagedObject uses exactly this: the accessors go through the managed object’s storage rather
than an ivar. See
Properties and Encapsulation.
Step 2: Fast Forwarding
If you have nothing to add but know who does, return that object:
@interface Facade : NSObject
@property (nonatomic, strong) Worker *worker;
@end
@implementation Facade
- (id)forwardingTargetForSelector:(SEL)selector {
if ([self.worker respondsToSelector:selector]) {
return self.worker; // the whole message is re-sent to worker
}
return [super forwardingTargetForSelector:selector];
}
@end
This is the cheapest forwarding option — the runtime simply re-sends the original message with the original
arguments and no NSInvocation is created. Use it whenever a single other object can handle the message.
Its limits: you cannot inspect or modify the arguments, cannot send the message to more than one target, and
cannot change the return value. Returning self (or nil) declines and moves on to step 3.
Step 3: Normal Forwarding
The full mechanism. It requires two methods, and both are mandatory — without a signature, the runtime cannot build the invocation and skips straight to failure.
@implementation Facade
// 1. Describe the message, so an NSInvocation can be built.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
NSMethodSignature *sig = [super methodSignatureForSelector:selector];
if (!sig) {
sig = [self.worker methodSignatureForSelector:selector];
}
return sig;
}
// 2. Do whatever you like with the packaged message.
- (void)forwardInvocation:(NSInvocation *)invocation {
if ([self.worker respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:self.worker];
} else {
[super forwardInvocation:invocation]; // ends in doesNotRecognizeSelector:
}
}
@end
Because you hold the message as an object, this step can do things the earlier ones cannot:
- (void)forwardInvocation:(NSInvocation *)invocation {
// Broadcast to several targets.
for (id target in self.observers) {
if ([target respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:target];
}
}
// Or log, time, or rewrite the message.
NSLog(@"forwarding %@", NSStringFromSelector(invocation.selector));
// Or synthesise a return value without invoking anything at all.
NSInteger fake = 42;
[invocation setReturnValue:&fake];
}
Two details matter in practice. Call [invocation retainArguments] if the invocation will outlive the current
scope (it does not copy its arguments by default). And override respondsToSelector: and
conformsToProtocol: to account for what you forward — otherwise callers that check first will never send
the message:
- (BOOL)respondsToSelector:(SEL)selector {
return [super respondsToSelector:selector] || [self.worker respondsToSelector:selector];
}
doesNotRecognizeSelector:
The end of the chain. NSObject’s implementation raises `NSInvalidArgumentException with the familiar text:
-[Facade fly]: unrecognized selector sent to instance 0x600000abc123
You can call it deliberately to mark an abstract method, as described in Inheritance and Polymorphism:
- (double)area {
[self doesNotRecognizeSelector:_cmd]; // "subclasses must override this"
return 0;
}
Overriding it to swallow unknown messages is a bad idea: typos and genuine bugs then vanish silently. If you need a "do nothing rather than crash" object, build it as an explicit null-object class rather than by disabling the runtime’s error reporting.
Proxies and Transparent Forwarding
NSProxy is a root class that implements almost nothing, so nearly every message it receives misses and
enters forwarding immediately. That makes it the natural base class for a stand-in.
@interface LazyProxy : NSProxy
@property (nonatomic, strong) id target;
@property (nonatomic, copy) id (^factory)(void);
@end
@implementation LazyProxy
- (instancetype)initWithFactory:(id (^)(void))factory {
_factory = [factory copy]; // NSProxy has no -init to call
return self;
}
- (id)target {
if (!_target) {
_target = self.factory(); // created on first actual use
}
return _target;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [self.target methodSignatureForSelector:selector];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
ExpensiveObject *obj = (ExpensiveObject *)[[LazyProxy alloc] initWithFactory:^id{
return [[ExpensiveObject alloc] initWithHugeDataSet:data];
}];
// Nothing has been constructed yet.
[obj doSomething]; // NOW the real object is created, and the message forwarded
NSProxy versus NSObject as the base: a proxy subclass of NSObject inherits well over a hundred methods,
so any message matching one of them is answered by NSObject instead of being forwarded — the wrapper leaks
through. NSProxy forwards essentially everything, which is what "transparent" requires. The cost is that
NSProxy has no init, no alloc conveniences and very little else, so you must be deliberate about
lifetime.
Common applications: lazy instantiation (above), remote objects, logging and profiling wrappers, mocks in
test frameworks such as OCMock, and weak-proxy objects that break NSTimer retain cycles.
Choosing a Step
| Use | When |
|---|---|
|
You can generate the implementation. Best performance — the cost is paid once, then it is an ordinary
cached method. This is how |
|
Exactly one other object should handle the message, unchanged. Cheap and simple; the right default for a wrapper. |
|
You need to inspect or rewrite arguments, broadcast to several targets, synthesise a return value, or log. The most powerful and the slowest. |
|
You want a fully transparent stand-in and must not inherit `NSObject’s implementations. |
And, as with everything in this corner of the language: these mechanisms defeat compile-time checking and make stack traces harder to read. Reach for an ordinary protocol, a delegate or a subclass first.
See Also
-
Messaging and Selectors — the lookup that precedes forwarding.
-
The Objective-C Runtime —
class_addMethod, type encodings andNSInvocation. -
Properties and Encapsulation —
@dynamic. -
Testing — mocking frameworks built on these hooks.