06
Apr 15
14:24
NSKeyedArchiver performance and custom class alternative
NSKeyedArchiver is a common choice for saving data of medium complexity, alongside plist serialization. More complex and you might use CoreData, less and you might use NSUserDefaults. I use NSKeyedArchiver in my Voicer app to record the metadata for sound. For the metadata I tried to design the format so that it would be very responsive, by only keeping index markers to audio samples in external files. However, I noticed for long sounds that were over an hour, it could take around 10 seconds to load just the metadata.
I thought about it longer, and decided that the reason for this must be the key-based lookup. So, I went ahead and removed all the keys (this is generally safe to do as long as you encode and decode in the same order). But, performance did not increase. I then tried using NSArchiver, which is listed in the mac os x docs as deprecated, and is not even mentioned in iOS docs. It is much faster. The only reference I found to it was an older StackOverflow post from 2012. However, using it is problematic, because presumably, it counts as private API which could be rejected on app submission. I put it down for a long time, but recently came back to it – I fixed up and renamed the Cocotron NSArchiver class to NSKeylessArchiver so that it could exist along side the private one. The results are okay, but could be improved.Writing directly to a file would be faster for my case, but I’m sticking with the initWithCoder method for now, because it is less work, but also for maintenance – the encode/decode functions are nice, and it’s easy to have backwards compatibility with NSKeyedArchiver by checking a file extension to determine the right Unarchiver to use. The NSKeylessArchiver also does some fancy but mostly-cheap stuff, such as string and object references – if you use the same string twice, it will only occur in the archiver once, and used as a reference the second time. Here is the github repo and the readme: | ||||||||||||
How to useAdd NSKeylessArchiver and NSKeylessUnarchiver’s .h and .m files to your project. If you have an arc project, you will need to go into the "Build Phases" tab of your project settings and add the ‘-fno-objc-arc’ flag to both NSKeylessArchiver.m and NSKeylessUnarchiver.m. Then, just #import "NSKeylessArchiver.h" and use it like an NSCoder. See the performance test project below for a sample. When to useIf you have a moderate to large use case for NSKeyedArchiver that does not rely on keys (e.g. uses only -[NSCoder encodeObject:] and -[NSCoder encodeObjectOfObjcType: at:] as opposed to -[NSCoder encodeObject: forKey:]), then you may be able to use this. The keyed based encoding and decoding is much slower, because it allows for random access. The non-keyed method allows a faster linear, forward-only read and write because it requires the order of encoding and decoding to be fixed. Mac OS X has a deprecated NSArchiver/NSUnarchiver, and iOS has this, but is not a public API. How do I get backward-compatibility without keys? Forward-compatibility?A frequent cited advantage of NSKeyedArchiver is that the keys provide easy backward-compatibility. It certainly helps, but of course, backward compatibility can exist without keys. Forward-compatibility via keys is much harder, but in the ‘save user data’ case, you may not need it – the only way that you will need to load a file from a future version is for cloud-based methods. PerformanceFirst of, if fast-as-possible performance is your goal, you might consider something else, such as directly writing to a binary file. This is a trade off for having the convenience of having a drop-in replacement for compliant uses of NSKeyedArchiver. For a test case, I compared NSKeyedArchiver, NSKeylessArchiver, and NSArchiver with a simple root object with 20000 ints. Please feel free modify the test repo and this class to improve performance and correctness. This was the result of running a release build on an iPhone 5S, with 10 runs per class:
As you can see the performance for this unrealistic use case shows NSKeylessArchiver doing much better than NSKeyedArchiver, but worse than NSArchiver. NSArchiver is a better choice, but apps that use it may not pass iOS app review due to the private API status. If you know otherwise, let me know. In practical use, I have seen about a 2-5x speed increase for a use case that had ~20,000 ints/floats/objects being encoded/decoded. |
For reference:
- BiasedBit also has a good overview and comparison of NSKeyedArchiver to NSPropertyListSerialization.
- mpv has an interesting post comparing NSKeyedArchiver to JSON serialization. I also considered this method, but went with NSCoder to be able to continue supporting various objects that implement NSCoding.