Method Swizzling

Recently I wrote about how to dynamically subclass objects in Objective-C, and when that might be useful.

While dynamic subclassing can be really useful, there is one major gotcha that can make it not very effective, and that was hinted at the end of my post: Key-Value Observing.

When you add an observer to an object, the KVO mechanism will dynamically subclass that object to implement the observation logic. For example, consider the following class:

@interface DataObject : NSObject

@property NSInteger dataValue;

@end

When you observe an instance of DataObject<, a dynamic subclass is created. This class is called NSKVONotifying_DataObject, but even that is hidden from you. The KVO subclass is devious; it hides its existence. Consider this:

DataObject * dataObject = [[DataObject alloc] init];

[dataObject addObserver:self forKeyPath:@"dataValue" options:0 context:NULL];

NSLog(@"%@", [dataObject class]);  //logs "DataObject"
NSLog(@"%@", object_getClass(dataObject));  //logs "NSKVONotifying_DataObject"

In other words, the class of an observed object does everything it can to look and behave exactly like the original class. For the most part, this is appropriate behavior. However, when you want to implement your own dynamic subclassing mechanism, it can wreak havoc. A dynamic KVO subclass does not like to be subclassed, because when you remove all the observers from the object, the class of the object is changed back to its original class. This means that even if you manage to dynamically subclass a KVO class, those changes will be lost when the class of the object is changed back to its original class.

So in the case of CHLayoutManager, a different mechanism for automatic cleanup was needed, since it is not uncommon to observe a UI element in order to react to state changes.

After some experimentation, I came up with an alternative: replace the -dealloc method of NSView with a new version.

Now, I don’t have the source to AppKit. I don’t know what NSView is doing under the hood. Fortunately, with Objective-C, that’s not really a problem, because there’s this really neat function in the runtime called method_exchangeImplementations, and it works like this:

+ (void) initialize {
  if (self == [CHLayoutManager class]) {
    Class nsview = [NSView class];

    SEL dynamicDealloc = @selector(chlayoutautoremove_dynamicDealloc);

    Method newDealloc = class_getInstanceMethod(self, dynamicDealloc);
    if (newDealloc != NULL) {
      class_addMethod(nsview, dynamicDealloc, method_getImplementation(newDealloc), method_getTypeEncoding(newDealloc));
      newDealloc = class_getInstanceMethod(nsview, dynamicDealloc);

      if (newDealloc != NULL) {
        Method originalDealloc = class_getInstanceMethod(nsview, @selector(dealloc));
        method_exchangeImplementations(originalDealloc, newDealloc);
      }
    }
  }
}

- (void) chlayoutautoremove_dynamicDealloc {
  if ([self isKindOfClass:[NSView class]]) { //to prevent people from being stupid
    [[CHLayoutManager sharedLayoutManager] removeConstraintsFromView:(NSView *)self];
    [[CHLayoutManager sharedLayoutManager] setLayoutName:nil forView:(NSView *)self];
    //THIS IS NOT A RECURSIVE CALL
    [self chlayoutautoremove_dynamicDealloc];
  }
}

This is a two step process:

First, we find the NSView class and add a new method to it. This method, called -chlayoutautoremove_dynamicDealloc simply performs cleanup with the layout manager singleton. After we add the method to NSView, we swap the implementations of -dealloc and -chlayoutautoremove_dynamicDealloc. This means that -[NSView dealloc] will invoke the implementation of -chlayoutautoremove_dynamicDealloc, and -[NSView chlayoutautoremove_dynamicDealloc] will invoke the original -dealloc code. Essentially, this is more or less injecting code into NSView that will be executed right before the -dealloc code gets executed.

Neat!

  1. funwithobjc posted this
Short URL for this post: http://tmblr.co/Zt522y1OObUw
blog comments powered by Disqus