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, instancetype, Clang modules, lightweight generics, nullability and @available — as published at Apple Developer Documentation and the Clang Objective-C specifications, which are the references these pages are written and verified against.

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

flowchart TD A["objc_msgSend: selector not found in the class
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

+resolveInstanceMethod:

You can generate the implementation. Best performance — the cost is paid once, then it is an ordinary cached method. This is how @dynamic properties work.

-forwardingTargetForSelector:

Exactly one other object should handle the message, unchanged. Cheap and simple; the right default for a wrapper.

-forwardInvocation:

You need to inspect or rewrite arguments, broadcast to several targets, synthesise a return value, or log. The most powerful and the slowest.

NSProxy

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