NSCoder subclass recreating NSArchiver

NSArchiver deprecated with OS X 10.2 and is not available AFAIK on iOS

On the other hand, it is known that NSKeyedArchiver lacks part of speed and conciseness ( some users report performance more than 100 times the difference between NSKeyedArchiver and NSArchiver ). The objects that I want to archive are mainly subclasses of NSObject containing NSMutableArray of NSNumber , and objects containing primitive types (mostly double ). I am not convinced that key invoices mean value.

So, I decided to subclass NSCoder on iOS to create a serial encoder in the style of NSArchiver .

I understand where archives with keys can be useful: backward compatibility and other subtleties, and probably this is what I end up using, but I would be interested to know what performances I can get with serial archiving. And frankly, I think I could learn a lot by doing this. Therefore, I am not interested in an alternative solution;

I was inspired by Cocotron sources, providing the open source NSArchiver

TL; DR : I want to subclass NSCoder rebuild NSArchiver


I am using ARC, compiling for iOS 6 and 7 and now assuming a 32-bit system.

At the moment, I'm not interested in references to objects or strings. I use only NSHashTable ( weakObjectsHashTable ) to prevent duplication of class names: classes will be described when they are first discovered, and then passed by reference.

I use NSMutableData to build the archive:

 @interface Archiver { NSMutableData *_data; void *_bytes; size_t _position; NSHashTable *_classes; } @end 

The main methods:

 -(void)_expandBuffer:(NSUInteger)length { [_data increaseLengthBy:length]; _bytes = _data.mutableBytes; } -(void)_appendBytes:(const void *)data length:(NSUInteger)length { [self _expandBuffer:length]; memcpy(_bytes+_position, data, length); _position += length; } 

I am using _appendBytes:length: to discard primitive types like int , char , float , double ... etc. There is nothing interesting.

C-style strings are discarded using this equally interesting method:

 -(void)_appendCString:(const char*)cString { NSUInteger length = strlen(cString); [self _appendBytes:cString length:length+1]; } 

And finally, archiving information and class objects:

 -(void)_appendReference:(id)reference { [self _appendBytes:&reference length:4]; } -(void)_appendClass:(Class)class { // NSObject class is always represented by nil by convention if (class == [NSObject class]) { [self _appendReference:nil]; return; } // Append reference to class [self _appendReference:class]; // And append class name if this is the first time it is encountered if (![_classes containsObject:class]) { [_classes addObject:class]; [self _appendCString:[NSStringFromClass(class) cStringUsingEncoding:NSASCIIStringEncoding]]; } } -(void)_appendObject:(const id)object { // References are saved // Although we don't handle relationships between objects *yet* (we could do it the exact same way we do for classes) // at least it is useful to determine whether object was nil or not [self _appendReference:object]; if (object==nil) return; [self _appendClass:[object classForCoder]]; [object encodeWithCoder:self]; } 

The methods of encodeWithCoder: my objects look like this, nothing fancy:

 [aCoder encodeValueOfObjCType:@encode(double) at:&_someDoubleMember]; [aCoder encodeObject:_someCustomClassInstanceMember]; [aCoder encodeObject:_someMutableArrayMember]; 

Decoding happens in much the same way; The unarchiver has NSMapTable classes that it already knows, and is looking for the name of a reference to a class that it does not know about.

 @interface Unarchiver (){ NSData *_data; const void *_bytes; NSMapTable *_classes; } @end 

I will not bore you with the specification

 -(void)_extractBytesTo:(void*)data length:(NSUInteger)length 

and

 -(char*)_extractCString 

Interesting material is probably in the object decoding code:

 -(id)_extractReference { id reference; [self _extractBytesTo:&reference length:4]; return reference; } -(Class)_extractClass { // Lookup class reference id classReference = [self _extractReference]; // NSObject is always nil if (classReference==nil) return [NSObject class]; // Do we already know that one ? if (![_classes objectForKey:classReference]) { // If not, then the name should follow char *classCName = [self _extractCString]; NSString *className = [NSString stringWithCString:classCName encoding:NSASCIIStringEncoding]; free(classCName); Class class = NSClassFromString(className); [_classes setObject:class forKey:classReference]; } return [_classes objectForKey:classReference]; } -(id)_extractObject { id objectReference = [self _extractReference]; if (!objectReference) { return nil; } Class objectClass = [self _extractClass]; id object = [[objectClass alloc] initWithCoder:self]; return object; } 

And finally, the central method (I won’t be surprised if the problem is somewhere here)

 -(void)decodeValueOfObjCType:(const char *)type at:(void *)data { switch(*type){ /* snip, snip */ case '@': *(id __strong *) data = [self _extractObject]; break; } } 

The initWithCoder: corresponding to the previous encodeWithCoder: will look something like this.

 if (self = [super init]) { // Order is important [aDecoder decodeValueOfObjCType:@encode(double) at:& _someDoubleMember]; _someCustomClassInstanceMember = [aDecoder decodeObject]; _someMutableArrayMember = [aDecoder decodeObject]; } return self; 

My implementation of decodeObject is definitely _extractObject .

Now all this should work well and well. And really; I can archive / unlock some of my objects. The archives look great as far as I am ready to test them in a hex editor, and I can unlock some of my custom classes containing NSMutableArray another class containing double s.


But for some reason, if I try to parse one of my objects containing NSMutableArray of NSNumber , I ran into this problem:

 malloc: *** error for object 0xc112cc: pointer being freed was not allocated 

There seems to be one row in the NSNumber in NSNumber , and the address 0xc112cc for each row. Putting a breakpoint in malloc_error_break tells me that the errors are from -[NSPlaceholderNumber initWithCoder:] (called from my _extractObject method).

Is this a problem with my use of ARC? What am I missing?

+6
source share
1 answer

My mistake was related to a misunderstanding of the second argument -(void)encodeValueOfObjCType:(const char *)type at:(const void *)addr in the case when type represents a C-string ( *type == '*' ). In this case, addr is const char ** , a pointer to const char * , which itself points to a constant, 0 complete char array that must be encoded.

NSNumber encodeWithCoder: encodes a small C-string representing the type of a variable that supports a value (a i for int , d for double, etc., this is equal to AFAIK directive @encode ),

My previous misinterpretation (assuming addr was const char * ) did the wrong encoding / decoding, and therefore initWithCoder: failed (on the bottom line: she tried the free stack variable, thus the message error and the fact that the address always the same for every function call).

Now I have a working implementation. If anyone is interested, the code is on my GitHub under the MIT license.

+2
source

All Articles