Dynamic Subclassing

If you’ve seen my github.com page or my portfolio link, you’re probably aware of a project of mine called CHLayoutManager. CHLayoutManager, for those of you not in-the-know, is a way to define the layout of a user interface on the Mac via constraints (as opposed to autoresizing masks). For example, if you have two buttons “A” and “B”, you can say “I want the right edge of button ‘A’ to always stay 10 points to the left of the left edge of button ‘B’”. Then, as button “B” moves around (via autoresizing masks or positioning it programmatically), button “A” automatically moves as well. If you do a lot of programmatic UI layouting, this can be insanely useful. In fact, a couple forthcoming products from Mozy will be using CHLayoutManager in them.

Internally, there’s a singleton object called the CHLayoutManager. This object is the one responsible for noticing when a view changes its frame, and then also determining if any other views need to change because of it. This layout manager also has an NSMapTable for storing all of the constraint information related to views. The key of the map table is the view itself, and the value is a container for storing the constraints and the layout name. Because this is supposed to operate silently in the background, the map table maintains a weak reference on the view, and a strong reference to the constraint container. That means if you’re using garbage collection and a view is deallocated, the garbage collector will automatically clean up the entry in the map table, and all will be well in the world.

However, if you’re not using garbage collection, some interesting behavior can crop up. For example, let’s say the map table contains a number of key-value pairs, and some of the keys point to views that have been deallocated (ie, the pointers are invalid). At some point in the future, when you attempt to add a constraint to a new view, the map table will need to resize itself. This means that it will reorganize its contents. Part of this step involves invoking -hash on each key.

But wait. Some of the keys are invalid pointers. If you try to send a message to a deallocated object, you’re (almost definitely) going to crash. Herein lies the crux of the problem: how can we ensure that each view automatically gets cleaned up from the map table without requiring the programmer to do it manually, and without relying on garbage collection?

The answer: dynamic subclassing.

When the user adds a constraint to a view, the layout manager is going to introspect this view and see if the view needs to be altered. If it does, then the manager is going to create a new class that’s a subclass of the view’s class. To this new class gets added a new method: a custom -dealloc method. This method performs the cleanup of the view’s constraints (and other layout information), then invokes [super dealloc]. Once we’ve created this subclass, we simply change the class of the view, and we’re good to go.

What does this look like? Like this:

- (void) dynamicallySubclassView:(NSView *)view {
  const char * prefix = "CHLayoutAutoremove_";
  Class viewClass = [view class];
  NSString * className = NSStringFromClass(viewClass);
  if (strncmp(prefix, [className UTF8String], strlen(prefix)) == 0) { return; }

  NSString * subclassName = [NSString stringWithFormat:@"%s%@", prefix, className];
  Class subclass = NSClassFromString(subclassName);

  if (subclass == nil) {
    subclass = objc_allocateClassPair(viewClass, [subclassName UTF8String], 0);
    if (subclass != nil) {
      IMP dealloc = class_getMethodImplementation([self class], @selector(dynamicDealloc));

      class_addMethod(subclass, @selector(dealloc), dealloc, "v@:");
      objc_registerClassPair(subclass);
    }
  }

  if (subclass != nil) {
    object_setClass(view, subclass);
  }
}

Here you can see what’s going on:

  1. Extract the view’s class
  2. See if the name of this class begins with the prefix used to indicate one of these dynamic subclasses
  3. If it doesn’t have the prefix, then build the name of the new class (+[NSString stringWithFormat:])
  4. Look in the runtime to see if a class of this name already exists
  5. If it doesn’t exist, create the class using objc_allocateClassPair()
  6. Add the custom -dealloc method to the new subclass
  7. Register the class with the runtime
  8. If everything went well, set the class of the view to the new subclass

So if you have an NSPopUpButton and add some constraints to it, it’s actually going to be an CHLayoutAutoremove_NSPopUpButton.

This is, incidentally, how Key-Value Observing is implemented in Cocoa and Cocoa Touch.

Isn’t Objective-C fun?

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