Speeding up NSCoding with Macros

Remember the days where you had to implement the dealloc method for every class?

Copying each instance variable from the header file and pasting it into dealloc was not only a pain the ass, it was a recipe for disaster. Forgot one instance variable? Memory leak. Had 30 or so instance variables for a class and accidentally released the same instance variable twice? Crash.

Fortunately, those days are over for memory management – but they still exist for archiving and unarchiving objects. How miserable is it writing the following code:

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if(self) {
        _val = [aDecoder decodeIntForKey:@"_val"];
        _obj = [aDecoder decodeObjectForKey:@"_obj"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:_obj forKey:@"_obj"];
    [aCoder encodeInt:_val forKey:@"_val"];
}

Not only is this code miserable to type, but it is exceptionally prone to error: what happens if you misspell a key in one of the methods? Either you don’t save some piece of data or don’t reload it, both of which are errors that won’t make themselves obvious. Instead, some other part of your application will stop working and it will take a little bit of time to trace the issue back to a simple typo in your NSCoding methods.

There is a very simple solution, though, and it relies on a relatively unknown feature of the C preprocessor: stringification. In any C file, and therefore any Objective-C file, you can create a macro that contains the # character in front of a symbol. The macro turns that symbol into a C string (char *).

For example, this:

#define STRINGIFY(x) #x
int myVariable = 5;
NSLog(@"%s", STRINGIFY(myVariable));

What’s that print? “myVariable”. Alright, what does this buy us?

Well, I can create another macro like this:

#define OBJC_STRINGIFY(x) @#x

Which means instead of getting a C string back, I get an NSString:

int myVariable = 5;
NSString *foo = OBJC_STRINGIFY(myVariable);
NSLog(@"%@", foo);

This, of course, prints out “myVariable” again. Why does this matter for NSCoding? Well, convention for encoding and decoding instance variables is to use the name of that instance variable as the key:

[aCoder encodeObject:_myInstanceVariable forKey:@"_myInstanceVariable"];

As you will notice, we are representing the same “string” in two ways in this line of code: once as a symbol for the compiler and once as an NSString. It would be much more simple and error-free to only write _myInstanceVariable once and have the compiler check it. With OBJC_STRINGIFY, that is easy:

#define OBJC_STRINGIFY(x) @#x
#define encodeObject(x) [aCoder encodeObject:x forKey:OBJC_STRINGIFY(x)]
#define decodeObject(x) x = [aDecoder decodeObjectForKey:OBJC_STRINGIFY(x)]

Now, your NSCoding methods can look like this:

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if(self) {
        decodeObject(_obj);
    }
    return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    encodeObject(_obj);
}

One of the things some folks don’t like about macros like this is that they can’t verify the code that is actually being written – false. With a .m file open, from the menubar, select Product -> Generate Output -> Preprocessed File. Xcode will generate what your implementation file looks like after all preprocessor directives have been evaluated.

Remember, though, that #import is a preprocessor directive. Therefore, a preprocessed .m file will contain every header file from Foundation, UIKit, and anything else you may have included – so be sure to scroll down to the bottom of the preprocessed result. (You may also notice that you can generate the assembly in a similar way, which is always interesting to check out.)

So, where do you define these macros like OBJC_STRINGIFY? Typically, I put them into a header file that has a lot of quick utility macros that I like to use in a lot of projects. Then, for each new project that I create, I import that header file into my pre-compiled header (the .pch file that comes with all of your projects). The pre-compiled header file for a project is transparently included in every file in a project. This means two things: anything in the .pch file is available in every file in your project and anytime you change the .pch file you have to re-compile your entire project.

Now, back to NSCoding for a brief moment: remember that when archiving, you may not be encoding or decoding just objects. You may be encoding an integer, float or even a structure. So, you will probably need macros like this as well:

#define encodeFloat(x) [aCoder encodeFloat:x forKey:OBJC_STRINGIFY(x)]

For structures, I typically decompose each member of a structure during archiving anyhow, so if I were encoding a CGPoint, for example, it would look like this:

encodeFloat(_point.x);
encodeFloat(_point.y);

Of course, don’t take my word that this code works (even though it does), write it yourself and generate the preprocessed file to see the result.

Say hello to your new mobile product team.

  • This field is for validation purposes and should be left unchanged.


0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *