PDA

View Full Version : NSCoder issue/clarification




mdeh
Jul 12, 2009, 02:11 PM
May I ask the advice of the experts here?

I have successfully added archiving to a non-doc based Application. (Yes...I am going through Hillegass, with a twist! :)

1) Adopted NSCoding in the model class

2) Call archiveRootObject like this.


- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
....preparatory stuff

[NSKeyedArchiver archiveRootObject: self toFile: savedValuesFile];


...more stuff }

3) call unarchiveObjectWithFile like this.


-(id) init
{

....preparatory stuff

return [NSKeyedUnarchiver unarchiveObjectWithFile:savedValuesFile];


}


Here is what puzzles me.

Both -(id) initWithCoder: (NSCoder *) coder
and -(void) encodeWithCoder: (NSCoder *) coder

are clearly called with the correct "coder" argument. I just wonder **how** these two methods receive the **correct** instance of coder. Particularly puzzling is the call in "init" which does not even handle the returned object, yet it obviously finds it's way to "initWithCoder". :-)


Thanks in advance, as usual



kpua
Jul 12, 2009, 03:36 PM
When you encode an object, its class name is encoded along with the data that your -encodeWithCoder: method writes.

When you decode an object, the encoder first reads what the class of an object is, gets its Class meta-object and sends it -alloc, so it has a valid instance of the new object that it can send -initWithCoder:.

The NSCoder instance passed to these methods is instantiated by the archiveRootObject:toFile: and unarchiveObjectWithFile: methods. That happens implicitly behind the scenes so you don't have to worry about it.

mdeh
Jul 12, 2009, 06:57 PM
Thanks kpua. If you don't mind, I'd like to explore this a little further, so that it becomes a little clearer conceptually to me.


When you encode an object, its class name is encoded along with the data that your -encodeWithCoder: method writes.

When you decode an object, the encoder first reads what the class of an object is, gets its Class meta-object and sends it -alloc, so it has a valid instance of the new object that it can send -initWithCoder:

I guess the term "meta-object" had me puzzled. Is this simply the same as, for instance MyClass *c = [[ MyClass alloc].......] without the init? So, what ever this step returns, is now used as part of the "coder" argument in the

"-(id) initWithCoder: (NSCoder *) coder"

method? In that way, "coder" contains the ability to respond to the selectors, as well as providing the necessary data needed in the unarchiving process?

In Apple's documentation, they give this example of unArchiving.


MapView *myMapView;
NSString *archivePath = ...get the path;
myMapView = [NSKeyedUnarchiver unarchiveObjectWithFile:archivePath];
The issue that I would like to understand is that they "use" the return from "unarchiveObjectWithFile:", whereas in my code, I just ignore this return value, and allow "initWithCoder" to be invoked, yet it does not seem to effect the App.

Apologise if you believe you have already answered this.

Much thanks

kpua
Jul 13, 2009, 01:38 AM
I'll let (pseudo-)code speak for itself, since it's often clearer than English.

I believe a simplified version of -unarchiveObjectWithFile: and -decodeObjectForKey: could look something like this:


+ (id)unarchiveObjectWithFile:(NSString *)file {
NSData *fileData = [NSData dataWithContentsOfFile:file];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:fileData];
id rootObject = [unarchiver decodeObjectForKey:@"ROOT"];
return rootObject;
}

- (id)decodeObjectForKey:(NSString *)key {
NSString *className = [self _readClassNameForKey:key];
Class metaClassObject = NSClassFromString(className);
id object = [[metaClassObject alloc] initWithCoder:self];
return object;
}


Don't try to compile that, it's just meant to illustrate.

So, here's what's happening:

+unarchiveObjectWithFile: creates a NSKeyedUnarchiver object with the contents of the given file and then decodes the root object and returns it.

-decodeObjectForKey: reads some hidden header information that contains the encoded type (int, float, data, object, etc.), and, (for objects) the class name. The class name is then used to get a 'Class' object, which is a meta-object (my terminology may not be 100% correct here), which can be sent +alloc to allocate an object. So, in other words, the following are essentially identical:

[[NSString alloc] init];

and

Class stringClass = NSClassFromString(@"NSString");
[[stringClass alloc] init];


This pattern is very powerful because it makes instantiation of objects, for which you may not know the class of until runtime. In other languages, you have to resort to cumbersome Factory patterns to accomplish that. One more reason to enjoy Objective-C. :-)

So, if your archive contains an instance of MapView, it reads "MapView" from the archive, gets its meta-class object, instantiates and object, then sends it the -initWithCoder: message. That initWithCoder: message will be dynamically dispatched so it reaches your -initWithCoder: implementation in MapView.

Understand? Cool stuff, huh?

(Archiving and unarchiving is actually quite a bit more sophisticated than this, since there are facilities that allow you to swap one class for another one at runtime and so forth.)

mdeh
Jul 13, 2009, 07:08 AM
I'll let (pseudo-)code speak for itself, since it's often clearer than English.




Thank you very much for that. Much appreciated.