Hi. My name is Alex Wiltschko.

This is my internet log.



28 June, 2011

The easy way to persist objects in Objective-C

I really do dig programming the iPhone, and the Objective-C language. I understand that working with the language has become much more convenient since its early days. As someone who has been working on the iPhone (and never the Mac, really) for only a couple years, I realized I’m a bit spoiled. But, I came to Objective-C from a scripting background, particularly Matlab and Python, where you could do crazy things like

saveas(gcf(), ‘an_easily_saved_image.png’);

and we got away with it, too. The point of this little post is to share an easy to use class that allows you to save or load your classes with just one line of code.

If you want to skip this post, get the code, and start using it,

You can get the AWEncoder class here.

This is how you’d use it:

MyObject *myObject = [[MyObject alloc] init];
MyObject.someFloatValue = 3.2;
[AWEncoder save:MyObject forKey:@"MyClass"];

...

// At some other point in your code, or even on another
// launch of your app, you might want to retrieve that instance
// just as you'd saved it before.
MyObject *anotherInstance = [[MyClass alloc] init];
[AWEncoder load:anotherInstance forKey:@"MyClass"];

What would you actually use this for? In several of my apps, I have a central preference manager that exists as a singleton, which I access like so:

PreferenceManager *sharedPrefs = [PreferenceManager sharedPreferences];
int someParameterSetByTheUser = sharedPrefs.theImportantParameter;

It’s a pretty convenient way to work, for the most part. However, persistently saving each and every little parameter inside of the preferences class creates a lot of code clutter and unnecessary work, which is how I use to do things. The mess looked a little like this:

- (void)setTheImportantParameter:(int)newValue
{
    theImportantParameter = newValue;
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setInteger:theImportantParameter forKey:@"theImportantParameter"];
    [defaults synchronize];
}

That’s completely excluding all of the setup code in the class’s init method that had to pull out each and every value that I’d saved. So, whenever I’d add a new preference, I’d have to add a getter in the init method

self.theImportantParameter = [defaults integerForKey:@"theImportantParameter"];

Well, that’s all profoundly annoying, not to mention difficult to maintain. So, I came up with a little scheme to change that. As I said before, all I have to do now whenever I’d like to persist my preferences class is type

[AWEncoder save:sharedPrefs forKey:@"Preferences"];

And, in the initialization of the SharedPreferences class, I just add one line

- (id)init
{
    self = [super init];
    if (self) {
        [AWEncoder load:self]; // just this one line
        return self;
    }
    return nil;
}

That’s it. It works for floats, ints, longs, NSStrings, whatever. It currently does not work for structs, or C arrays of any kind. If you’d like to figure that out, that’d be neat! All of the properties get stored in NSUserDefaults, which persists between launches of the application, and is supposed to be threadsafe. Mind you, though, I haven’t tested this particular method for thread-safeness quite yet. Again, if you’d like to try to break the AWEncoder class in a weird thread-dependent way, I’d love to know how you did it, and how to fix it.

So how does it work? Turns out, the Objective-C runtime will let you get all of the names and types of properties in a class. You can iterate over each property like this:

unsigned int outCount, i;
// Get every available property for the object we've received
objc_property_t *properties = class_copyPropertyList(objectToEncodeClass, &outCount);
for (i = 0; i < outCount; i++) {
    objc_property_t property = properties[i];

    ... // figure out what to do with the property here

}

So, given a property (of type objc_property_t), how do we figure out what type it is, and what its name is?

const char * propertyAttributes = property_getAttributes(property);

The property attributes are returned in a C string that’ll look like “Ti,VnameOfAVariable”. It can look quite a bit hairier, though, with structs and complicated objects. But, for the most part, The syntax is “T{single-character type}, V{name of the variable}”. For more info on possible property types, check out “Declared Properties” in the “Objective-C Runtime Programming Guide”. Anyways, for now, we just care about that second character in the string, along with the property’s name. Here’s how we do that:

char propertyTypeCchar            = property_getAttributes(property)[1];
const char * propertyNameCString  = property_getName(property);
NSString *propertyName            = [NSString stringWithCString:propertyNameCString encoding:NSUTF8StringEncoding];

Note that we grab a C string and NSString representation of the property’s name. That’ll come in handy later. So, we’ve got the variable’s type and name, and now, we’ll first cover encoding, then decoding the properties.

Encoding Properties

So, in order to do any encoding, we have to have first set up a coder. We’ll use the NSKeyedArchiver for encoding (and later, the NSKeyedUnarchiver for decoding). This gets set up outside our for-loop over the properties.

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedData = [defaults objectForKey:key];
NSKeyedUnarchiver *coder = [[NSKeyedUnarchiver alloc] initForReadingWithData:encodedData];

for (i = ... // the for loop iterating over our properties
             // and all our wonderful parsing code

The coder is what will actually be turning our properties into a format that can be stored on the device’s disk.

So, we’re back inside the for loop, we have a coder ready to serialize our properties. If the property is an Objective-C object, it’s pretty straight-forward.

for (i = ... // we're inside the for loop now

    ... // here's where we got the property type and name

    if (propertyTypeCchar == '@') {
        id object_property = [objectToEncode valueForKey:propertyName];
        [coder encodeObject:object_property forKey:propertyName];
    }

That was easy. But what about primitive types, like floats and ints? Those turn out to be a bit harder to grab. But there is a way, and this is it. First, we grab an abstract representation of the property as an “ivar”, or instance variable.

for (i = ... 

    ... // if the variable was an object, we've already dealt with it

    else if (propertyTypeCchar == '^') {
        // pointer
        // we don't deal with that
    } else if (propertyTypeCchar == '{}) {
        // struct
        // we ignore that, too
    } else {
        // primitives, like floats

        // 1. 

        Ivar ivar = object_getInstanceVariable(objectToEncode, propertyNameCString, NULL);
        // 2. 

        void *pointer_to_value = (void *)(objc_unretainedPointer(objectToEncode) + ivar_getOffset(ivar));
        // 3. 

        if (valuepointer) {
            [coder encodeValueOfObjCType:&propertyTypeCchar at:valuepointer];
        }

    }

} // the end of the for loop here

(Remember, ‘@’ means Objective-C object, ‘&’ means a buffer or function pointer, and ‘{’ means struct. We’ve already handled the ObjC types, and we’re going to completely ignore structs, pointers and buffers.)

So what’d we do there?

  1. First, we got a basic representation of our property as an instance variable, or ivar. The ivar doesn’t directly contain our data, but instead holds its name, its type, and its location in memory.

  2. We can get the exact location of the data we care about by taking the address location of our object, and then adding the offset of the property we want. This gives us a memory address.

  3. With our memory address in hand, we tell our coder to grab the data from that location in memory, and encode it.

So, we loop around and around all of our properties, until we’re all done, and then clean up:

for (i = ...

    // .. all our saving and encoding stuff

} // the end of the for loop from above

[coder finishEncoding];
[defaults setObject:data forKey:key];
[defaults synchronize];

We tell the defaults to save a chunk of data that we’ve encoded into a binary format. That’s it for encoding!

Decoding Properties

So how do we get those properties back out later? Decoding, dude. The process is mostly similar, with only a few changes. First, the coder is no longer NSKeyedArchiver, but NSKeyedUnarchiver, and we have to initialize it with the data we encoded from before.

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *encodedData = [defaults objectForKey:key];
NSKeyedUnarchiver *coder = [[NSKeyedUnarchiver alloc] initForReadingWithData:encodedData];

Earlier, we encoded two distinct types: objects and primitives. Now, we’ll decode them both. First, the objects.

if (propertyTypeCchar == '@') {
    // objects
    id object_property = [coder decodeObjectForKey:propertyName];
    [objectToDecode setValue:object_property forKey:propertyName];
}

Simple, as before. Now, the primitives are again, a little weird.

else {
    // primitives
    char var_pointer[64]; // more than enough room for any kind of primitive
    [coder decodeValueOfObjCType:&propertyTypeCchar at:var_pointer];
    object_setInstanceVariable(objectToDecode, propertyNameCString, *(void **)var_pointer);
}

What’s that crazy *(void **)var_pointer crap? I’d tell you not to worry about it, but maybe you’d be insistent, and keep asking. Well, if you must now, as far as I can tell, the decodeValueOfObjCType:at: selector wants a memory address, and it sticks the value we want to decode at that pointer location. But, for some reason I don’t thoroughly understand, object_setInstanceVariable() seems to want the value itself, and the only way to wrangle the buffer to not cause a compiler error is to add some serious pointer notation. If it looks hacky, it’s because it is.

Okay, so we’ve gotten our properties out, and back into the class we want. Do we have to do anything with the decoder, or the data from NSUserDefaults? Nope. We’re done. That’s decoding!

Improvements?

You could get really fancy and store the values in the Keychain, but that’s a huge can of worms that I’ve stuck my hand in once. Worms bite, it turns out. Also, as I mentioned, it’s not thoroughly tested, although I use it for my apps now. If you figure out bugs, wanna say “hello!”, or have suggestions, let me know on twitter (I’m @awiltsch). Again, the code can be found as a gist on github.

The AWEncoder class, check it out here.

Search it. Browse it. Subscribe it. Get caught up in it.


Get the RSS feed! Go ahead.