NSKeyedArchiver and NSKeyedUnarchiver with NSMutableArray
Asked Answered
M

4

6

I'm hoping this isn't something to do with the fact that I'm using a Mutable array here, but this one is baffling me so it wouldn't surprise me if that were the case.

BACKGROUND:

I have made a small database which is essentially an NSMutableArray containing custom objects, which we can call recordObjects. I set up the array:

database = [[NSMutableArray alloc] init];

and my custom object, called "recordObject" contains the following variables and inits:

NSString *name;
int       anInt;
Bool      aBool;

I also synthesized methods so I can make calls like:

aString = [[database objectAtIndex:someIndex] name];

And added methods to my controller class to add, remove, and select the individual records for display. So far everything works correctly and exactly as expected.

Next, I've set up my recordObject class (subclass of NSObject) to use the NSCoder (by including in the @interface directive, and have added the following custom encoder and decoder methods in the implementation file:

-(void) encodeWithCoder: (NSCoder *) encoder {
    [encoder encodeObject: name  forKey: @"recordName"];
    [encoder encodeInt:    anInt forKey: @"recordInteger"];
    [encoder encodeBool:   aBool forKey: @"recordBool"];
}

-(id) initWithCoder: (NSCoder *) decoder {
    name    = [decoder decodeObjectForKey: @"recordName"];
    anInt   = [decoder decodeIntForKey:    @"recordInteger"];
    aBool   = [decoder decodeBoolForKey:   @"recordBool"];
}

In order to write the file, I have used the following:

[NSKeyedArchiver archiveRootObject:database toFile:myPath];

When I run the program, everything APPEARS to work correctly. The encoder method is called for each of the records in the array and the file is written to disk. Opening the file with TextEdit shows that the data is there (though mostly unintelligible to me.)

THE PROBLEM:

Here's where I run into a snag.

I added the following code to LOAD the file into my database array:

database = [NSKeyedUnarchiver unarchiveObjectWithFile:myPath];

When I run the program again, this time Loading the database, it APPEARS to work correctly. My first test was to use the NSMutableArray count method:

x = [database count];

The result was that X is filled with the correct number of records in the file. If there were 5 records when I saved the database, X is set to 5 after loading the database on the next execution of the program.

Now, here's the big problem:

The program crashes if I try to use ANY of my accessor methods. For example, if I try to use the following after loading the database:

aString = [[database objectAtIndex:someIndex] name];

the program crashes and returns the following error in the console:

Program received signal:  “EXC_BAD_ACCESS”.
sharedlibrary apply-load-rules all

My interpretation is that the data is not being loaded and initialized into the database array correctly for some reason, but for the life of me I can't figure out where I've gone wrong here.

As a side note, everything I've implemented came from Stephen G. Kochan's book "Programming in Objective-C"

Any ideas would be greatly appreciated!

Moramorabito answered 30/4, 2012 at 23:23 Comment(0)
R
14

There are a few problems with your code.

Your initWithCoder: method is not fully implemented. You must call [super init] and return self. You must also copy or retain the string object, otherwise it will be autoreleased:

- (id)initWithCoder:(NSCoder *)decoder 
{
    self = [super init];
    if(self)
    {
         name    = [[decoder decodeObjectForKey: @"recordName"] copy];
         anInt   = [decoder decodeIntForKey:    @"recordInteger"];
         aBool   = [decoder decodeBoolForKey:   @"recordBool"];
    }
    return self;
}

The other problem is with this line:

database = [NSKeyedUnarchiver unarchiveObjectWithFile:myPath];

That is fine, except for two things:

  1. You're not holding a reference to the object, so it will be autoreleased.
  2. NSKeyedArchiver returns an immutable object, in this case an NSArray and not an NSMutableArray.

You need to do this:

database = [[NSKeyedUnarchiver unarchiveObjectWithFile:myPath] mutableCopy];

That will both retain the object (because it's a copy) and make the object an NSMutableArray.

Reiko answered 30/4, 2012 at 23:57 Comment(4)
Wouldn't a -copy suffice? According to docs, unarchiveObjectWithFile: "returns object graph previously encoded by NSKeyedArchivier" In this case that was an NSMutableArray.Walkin
No, if you call copy on an NSMutableArray then you'll get an immutable NSArray. Unfortunately here the docs are only telling half the story. The same thing occurs with NSCoder. If you use NSCoder to encode an NSMutableString then when you decode it you'll get back an NSString. You must use mutableCopy to get mutable versions back.Reiko
Your point about archiving mutable objects is incorrect. Archiving and unarchiving a mutable object such as an NSMutableArray or an NSMutableString does not make it immutable.Cynic
Like the previous comment, unarchiving a mutable object creates a mutable object, i tried it. Maybe it's was a bug in the previous SDK that was corrected.Swellfish
W
4

It doesn't appear that you're initializing the recordObjects in -initWithCoder:

Try something like this:

-(id) initWithCoder: (NSCoder *) decoder {

    self = [super init];

    if (self){

       name    = [decoder decodeObjectForKey: @"recordName"] copy];
       anInt   = [decoder decodeIntForKey:    @"recordInteger"];
       aBool   = [decoder decodeBoolForKey:   @"recordBool"];
     }

    return self;
}

The data is there when you archive it but you're not properly unarchiving it.

Walkin answered 30/4, 2012 at 23:28 Comment(6)
I'll give that a shot and report back. Thanks.Moramorabito
The important part is that this suggestion ends with 'return self'. Yours is missing that. The super init call is required too though.Wick
No good. I even tried using [[super alloc] init]; but still gives the same error.Moramorabito
Post your entire archiving / unarchiving code as well as the code for your myPath stringWalkin
Got it working. Definitely a group effort...lol Had to RETAIN the NSSTring variables as well. Thanks for the help!Moramorabito
Not sure if you can change answers, but Rob's explanation is much more complete.Walkin
C
2

Sounds like a memory management issue to me. EXC_BAD_ACCESS usually means that you're trying to access an object that has been deallocated. unarchiveObjectWithFile: returns an autoreleased object, so you have to retain it if you want to keep it around, either with an explicit retain or by assigning it to a retained property.

Camarena answered 30/4, 2012 at 23:26 Comment(9)
Hm. That would explain it (I'm EXTREMELY bad with memory management at this point... still trying to wrap my mind around it.)Moramorabito
Based on the code above, do you see, or could you recommend a way to Retain the database array? I'll re-read the chapter on Memory Management again tonight and see if I can figure it out as well, but if there's a simple fix, it would be greatly appreciated! :)Moramorabito
[database release]; database = [[NSKeyedUnarchiver unarchiveObjectWithFile:myPath] retain];Camarena
It's a memory issue because you are messaging objects not properly initialized.Walkin
Just noticed that you also don't retain your instance variables in your initWithCoder: method, you need to do that as well.Camarena
Depends on if he's using ARC, iVars are strong by default in ARC.Walkin
@BrianPalma If he's using Xcode 3.2.6, he's definitely not using ARC.Camarena
Sounds like I need to go back for a full primer on memory management... ugh. Though I had at least a basic understanding of it, but seeming like I need more time in the books. By the way, I implemented all of the suggestions above EXCEPT the most recent of retaining the instance variables (not sure how to do that...looking it up now) but still not working.Moramorabito
BINGO. Got it. Added a RETAIN tag to each of the NSString instance variables (apparently INT and BOOL don't like RETAIN) as well as the above suggestions, and now everything works correctly. I'm guessing one of my big problems is that I have the newest version of the book which is written with ARC in mind, and I don't have ARC in the version of XCODE I'm working with. Thanks for all the help guys!Moramorabito
A
0

I was having the same issue when unarchiving a custom object

self.calTable = [[NSKeyedUnarchiver unarchiveObjectWithFile:calibrationFile] objectForKey:@"calTable"];

Based on Rob's answer I changed to

self.calTable = [[[NSKeyedUnarchiver unarchiveObjectWithFile:calibrationFile] mutableCopy] objectForKey:@"calTable"];

and it fixed all errors.

Abc answered 7/8, 2014 at 19:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.