Why is Objective-C designed to have a three-layer based messaging mechanism?

Viewed 79

As far as I know, Objective-C has a three-layer based messaging mechanism:

  1. +(BOOL)resolveInstanceMethod:(SEL)sel or +(BOOL)resolveClassMethod:(SEL)sel
  2. -(id)forwardingTargetForSelector:(SEL)aSelector
  3. -(void)forwardInvocation:(NSInvocation *)anInvocation

My question is why do we need all of the three layers? It seems that the second layer and the third layer have something in common.

1 Answers

The full system is described in great detail in the Objective-C Runtime Programming Guide, particularly in the Messaging, Dynamic Method Resolution, and Message Forwarding sections.

The short answer is: incredible flexibility, coupled with speed in the common cases. Messages handling is incredibly powerful in ObjC. You can create all kinds of elaborate behaviors at runtime to respond to messages. But that functionality, while critical to many important parts of the system, isn't used very often in day-to-day app development. In the overwhelming majority of cases, as an app developer, you expect your message passing to be dispatched to a method that has the same name. The system optimizes that case. If it didn't, calling a method would take hundreds of times longer than it does.

objc_msgSend has a fast path in assembly that handles the most common-of-common cases. If this is a regular method that you've called before, it just pulls it from its cache and jumps to it. If it doesn't find it, it also can look up the method on the class and handle inheritance. But what if it isn't there at all?

In that case, the system gives the object several more options. The first is Key Value Coding, which looks for an accessor. But if that doesn't work it goes to the dynamic dispatch options, which you've enumerated. It can (1) invent a new method dynamically, (2) hand the message to another object, or (3) handle it some other way.

The first case supports things like dynamic loading. You can't do this on iOS, but on Mac, you can use resolveInstanceMethod to load a library from disk. It also allows you to dynamically create properties that are still fast (as opposed to using Key Value Coding, valueForKey:, etc, which is slower). Here's one example:

#import "Person.h"
#import <objc/runtime.h>

@interface Person ()
@property (strong) NSMutableDictionary *properties;
@end

@implementation Person
@dynamic givenName, surname;

- (id)init {
  if ((self = [super init])) {
    _properties = [[NSMutableDictionary alloc] init];
  }
  return self;
}

static id propertyIMP(id self, SEL _cmd) {
  return [[self properties] valueForKey:
          NSStringFromSelector(_cmd)];
}

static void setPropertyIMP(id self, SEL _cmd, id aValue) {
  id value = [aValue copy];
  
  NSMutableString *key =
  [NSStringFromSelector(_cmd) mutableCopy];
  
  // Delete "set" and ":" and lowercase first letter
  [key deleteCharactersInRange:NSMakeRange(0, 3)];
  [key deleteCharactersInRange:
   NSMakeRange([key length] - 1, 1)];
  NSString *firstChar = [key substringToIndex:1];
  [key replaceCharactersInRange:NSMakeRange(0, 1)
                     withString:[firstChar lowercaseString]];
  
  [[self properties] setValue:value forKey:key];
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
  if ([NSStringFromSelector(aSEL) hasPrefix:@"set"]) {
    class_addMethod([self class], aSEL,
                    (IMP)setPropertyIMP, "v@:@");
  }
  else {
    class_addMethod([self class], aSEL,
                    (IMP)propertyIMP, "@@:");
  }
  return YES;
}
@end

This object can accept any property name you like, and it will automatically create a getter and setter for it the first time you try to access the property. (I can't completely remember, but I think this is is how Core Data is implemented.)

Next, the system will allow you to forward the message to another object. This is the core feature of NSProxy, and proxy objects in general. Proxy objects are central to things like distributed messaging (where the "real" object might be in another process, or even on another hots).

And finally, if nothing else meets your needs, you can implement messageSignatureForSelector and forwardInvocation and do literally anything you want with the message. For example, you might make a trampoline that wraps another object, but runs any requested methods on the main thread:

@interface RNMainThreadTrampoline : NSObject
@property (nonatomic, readwrite, strong) id target;
- (id)initWithTarget:(id)aTarget;
@end

@implementation RNMainThreadTrampoline

- (id)initWithTarget:(id)aTarget {
    if ((self = [super init])) {
        _target = aTarget;
    }
    return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation setTarget:self.target];
  [invocation retainArguments];
    [invocation performSelectorOnMainThread:@selector(invoke)
                               withObject:nil
                            waitUntilDone:NO];
}
@end

The last time I profiled it, using forwardInvocation was about 500 times slower than a simple method call, so this is not a tool to use generally, but it's really powerful when you need it.

In the vast majority of cases, you won't need any of these things, but they exist for the parts of the system that do.

Related