Home > Articles > Home & Office Computing > Mac OS X

📄 Contents

  1. Key-Value Coding
  2. Prototypes, Mix-ins, and Contracts
  3. Summary
  • Print
  • + Share This
Like this article? We recommend

Prototypes, Mix-ins, and Contracts

If you've programmed in Self or JavaScript, you may have noticed that it's sometimes very useful to use the prototype mechanism to specialize a single instance of an object. Alternatively, you may have found that it's convenient to apply the same set of methods to different classes, in the form of a mix-in. These techniques are both possible in Objective-C; I implemented both before Apple produced the new runtime interfaces, but the new interfaces make them much simpler.

Another very useful thing you can implement on top of Objective-C is design-by-contract, in which you enforce a set of preconditions or post-conditions on a method. Objective-C provides a standard way of adding methods to an existing class (via categories), but that doesn't make it easy to wrap existing methods. With the runtime system, however, we can wrap existing methods. Take a look at this simple class:

@interface FakeClass : NSObject @end
static IMP oldRelease;
@implementation FakeClass
+ (void)load
    IMP newRelease =
        class_getMethodImplementation(self, @selector(release));
    Method release =
        class_getInstanceMethod([NSObject class], @selector(release));
    oldRelease = method_setImplementation(release, newRelease);
- (void)release
    NSLog(@"%@ released", self);
    oldRelease(self, _cmd);

The +load method is called when the class is loaded. This replaces the NSObject implementation of -release with the version declared on this class. So far, this sounds just like a category. The difference comes from the fact that it then keeps a pointer to the old implementation in a file-static variable. The new -release method now logs the object being released and then calls the real -release method.

This replacement lets you call the original method at the start, at the end, or in the middle of your new method. You can test that the arguments match a set of preconditions, test that the result matches a post-condition, test that invariants didn't change, or anything else you can think of. Because this substitution happens at runtime, you can even turn it on and off on a per-method basis while the application runs. Or, if you don't want very strenuous testing, have the method automatically replace itself with the original after the test has passed a few times.

For more-complex manipulations, the two important functions are objc_allocateClassPair() and objc_registerClassPair(). The names of these functions are quite confusing if you're not familiar with Smalltalk or the internals of the runtime library. Each class that you declare in Objective-C is really two classes. Every object is an instance of a class, and any messages sent to the object are mapped to methods by the class. Similarly, the class is an instance of a metaclass, and any messages sent to the class are mapped to methods by the metaclass. All of your instance methods are attached to the class and all of the class methods to the metaclass. When you allocate a class pair, you get one of each.

Apple uses this setup in a number of places. One of the most obvious is in key-value observing (KVO). When you call a KVC-compliant accessor in a class that's being observed by another class, a series of notifications is fired automatically. If you've ever looked at the isa pointer for an observed class, you may have noticed that it's not the value you'd expect.

When you first observe a class with KVO, Cocoa does a trick called isa-swizzling. The isa pointer is set to a new class, inheriting from the real class, and the accessor methods being observed are replaced by something that calls -willChangeValueForKey: and -didChangeValueForKey: before and after calling the implementation in the superclass.

Implementing something like this is different from the method substitution we saw earlier, because we only want to do the replacement for a single instance of the class. The first step is to construct the new class, like this:

struct hiddenClassIvars
        id object;
        NSMutableDictionary *otherData;
Class hiddenClass = objc_allocateClassPair(objc_getClass(object),
        "NewUniqueName", sizeof(struct hiddenClassIvars));

Note that the constant string here is only for demonstration. In real code, you must use a unique string for every call. Typically, you would use a private prefix and then a single number that you increment each time you create a new class. Notice the third parameter, which indicates the amount of space to allocate after the class. Because we don't need to add any instance variables to this class, we can register it and set the isa pointer immediately:

object_setClass(object, hiddenClass);

We should also set up the class data. This is important for transforms that may be performed on an object more than once. For example, in an implementation of prototype-based object orientation on Objective-C, you may need to attach a new class to an object, which is then used as a prototype for another object, so there are two hidden classes in the chain. The object pointer after the class lets you find which one was allocated for this class:

struct hiddenClassIvars *ivars = object_getIndexedIvars(hiddenClass);
ivars->object = object;
ivars->otherData = [NSMutableArray new];

The only step remaining is to add some methods. One that's easy to forget is -dealloc. Unless you're using garbage collection, you need to clean up this class once it's no longer referenced. The easiest way of writing a deallocation method like this is to attach it to another class and just copy the IMP from there. The method itself needs to find the hidden class and then deallocate it. To do this, you need to write a loop that goes up the class chain using class_getSuperclass() until it finds a class that has the right prefix name (so you know that it has the right instance variable structure) and the right object pointer. Once you've found it, you can just call objc_disposeClassPair() to free it up.

In the middle, you can add methods to the class just as we did earlier.

  • + Share This
  • 🔖 Save To Your Account