objective-cmemorynskeyedarchiver

Extracting Data from NSKeyedArchiver in Non-ARC Code


In my non-ARC iOS project, I have a method that returns archived data:

- (NSData*) archivedData {
    NSMutableData* data = [[NSMutableData alloc] init];
    NSKeyedArchiver* archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    // Encode the fields that must be archived:
    [archiver encodeObject:... forKey:...];
    ...
    [archiver finishEncoding];
    [archiver release];
    return [data autorelease];

Since initForWritingWithMutableData: is deprecated, I modified the implementation:

- (NSData*) archivedData {
    NSKeyedArchiver* archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES];
    // Encode the fields that must be archived:
    [archiver encodeObject:... forKey:...];
    ...
    [archiver finishEncoding];
    NSData* data = [archiver encodedData];
    [archiver release];
    return data;

At first, I called autorelease on the encoded NSData object before returning it but that resulted in a bad memory access (EXC_BAD_ACCESS). Everything seems to work fine without autorelease.

I am now confused. Since I release the archiver before returning the data, I thought autorelease would protect the NSData object, deallocating it not before it is processed by the calling method. I worried that the NSData object might be deallocated right after the archiver is released without the autorelease call. Somehow, the opposite happens when I run the code.

Could anybody please shed some light on this behavior? Also, if I'm doing something wrong, I'd like to know how to fix the code.

I'm aware of the static method archivedDataWithRootObject:requiringSecureCoding:error: but I can't easily use it because I don't have a root object as I encode objects individually. Using a root object would break compatibility for existing users of the app (if I understand it correctly).


Solution

  • NSData* data = [archiver encodedData];
    

    The method -encodedData does not begin with alloc or new, nor does it include the word copy, nor it it called retain. That means you did not take ownership of it. You must not call release on it. It doesn't belong to you. Said another way: you have not put a retain on this object; you must not call release on it.

    You then release archiver, which was responsible for data. There is no promise that data exists anymore. The fix for this is to retain it before releasing archiver:

    NSData* data = [[archiver encodedData] retain];
    

    This is the effectively the same thing the original code did by initializing data using +[NSMutableData alloc]. Note the word "alloc." This gives ownership (a retain) to data.

    By the same naming conventions, this method promises to return an object that the caller doesn't need to release. (It's often easiest to think of this as having a net retain count of 0, but there are many cases where that isn't strictly true.)

    - (NSData*) archivedData { ... }
    

    You have put a retain on the data, so you need to balance that out. You want the object to survive long enough to be returned, however. The fix for that is -autorelease, just like in the original code:

    return [data autorelease];
    

    So all together:

    - (NSData*) archivedData {
        NSKeyedArchiver* archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES];
        // Encode the fields that must be archived:
        [archiver encodeObject:... forKey:...];
        ...
        [archiver finishEncoding];
        NSData* data = [[archiver encodedData] retain]; // Retain here
        [archiver release];
        archiver = nil; // It's good practice to nil values that are no longer valid
        return [data autorelease]; // autorelease here
    }
    

    I can't find Apple's helpful Memory Management Rules page anymore, but it's summarized in Three Magic Words.