In Objective-C terms, archiving is the process of saving one or more objects in a format so that they can later be restored. Often, this involves writing the object(s) to a file so it can subsequently be read back in. We will discuss two methods for archiving data in this chapter: property lists and coding.
Applications on Mac OS X use lists (or plists) for storing things such as your default preferences, application settings, and configuration information, so it's useful to know how to create them and read them back in. Their use for archiving purposes, however, is limited because when creating a property list for a data structure, specific object classes are not retained, multiple references to the same object are not stored, and the mutability of an object is not preserved.
If your objects are NSString
, NSDictionary
, NSArray
, or NSData
objects, you can use the writeToFile:
method implemented in these classes to write your data to a file. In the case of writing out a dictionary or an array, this method writes the data out in the format of a property list. On the Mac, the property list created is in a format known as XML by default, which is an HTML-like language.1 On GNUStep systems, a different format for property lists, known as the traditional, old-style, or ASCII (depending upon whether you're speaking to GNU or Mac programmers) format is used by default.
Program 19.1 shows how the dictionary you created as a simple glossary in Chapter 15, “Numbers, Strings, and Collections,” can be written to a file as a property list.
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSDictionary *glossary =
[NSDictionary
dictionaryWithObjectsAndKeys:
@"A class defined so other classes can inherit from it.",
@"abstract class",
@"To implement all the methods defined in a protocol",
@"adopt",
@"Storing an object for later use. ",
@"archiving",
nil
];
if ([glossary writeToFile: @"glossary" atomically: YES] == NO)
printf ("Save to file failed!
");
[pool release];
return 0;
}
The writeToFile:atomically:
message is sent to your dictionary object glossary
, causing the dictionary to be written to the file glossary
in the form of a property list. The atomically
parameter is set to YES
, meaning you want the write operation to be done to a temporary backup file first and once successful, the final data is to be moved to the specified file named glossary
. This is a safeguard that protects the file from becoming corrupt if, for example, the system crashes in the middle of the write operation. In that case, the original glossary
file (if it previously existed) isn't harmed.
If you look at the glossary
file created by Program 19.1, it looks like this on the Mac:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>abstract class</key>
<string>A class defined so other classes can inherit from it.</string>
<key>adopt</key>
<string>To implement all the methods defined in a protocol</string>
<key>archiving</key>
<string>Storing an object for later use. </string>
</dict>
</plist>
As noted, under GNUStep, the property list is written in a traditional format by default. The glossary
file looks like this:
{
"abstract class" = "A class defined so other classes can inherit from it.";
adopt = "To implement all the methods defined in a protocol";
archiving = "Storing an object for later use. ";
}
GNUStep programs can generate XML-style property lists by changing the user default settings GSMacOSXCompatible
or NSWriteOldStylePropertyLists
. Look at the NSUserDefaults
class and check your GNUStep documentation for more details. Mac users can create traditional or old-style property lists using the dataFromPropertyList:format:errorDescription:
method from the NSPropertyListSerialization
class and specifying NSPropertyListOpenStepFormat
as the format
parameter. The Mac and GNUStep systems can read property lists created in either format.
If you create a property list from a dictionary, the keys in the dictionary must all be NSString
objects. The elements of an array or the values in a dictionary can be NSString
, NSArray
, NSDictionary
, NSData
, NSDate
, or NSNumber
objects.
To read a property list from a file into your program, you use the dictionaryWithContentsOfFile:
or arrayWithContentsOfFile:
method. To read back data, use the dataWithContentsOfFile:
method, and to read back string objects, use the stringWithContentsOfFile:
method. Program 19.2 reads back the glossary written in Program 19.1 and then prints it contents.
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSDictionary *glossary;
NSEnumerator *keyEnum;
id key;
glossary = [NSDictionary dictionaryWithContentsOfFile: @"glossary"];
keyEnum = [glossary keyEnumerator];
while ( (key = [keyEnum nextObject]) != nil ) {
printf ("%s: %s
", [key cString],
[[glossary objectForKey: key] cString]);
}
[pool release];
return 0;
}
archiving: Storing an object for later use.
abstract class: A class defined so other classes can inherit from it.
adopt: To implement all the methods defined in a protocol
Your property lists don't need to be created from an Objective-C program; the property list can come from any source. You can make your own property lists using a simple text editor or use the Property List Editor program located in the /Developer/Applications
directory on Mac OS X systems.
If you have a property list stored in an NSString
object, you can convert it into its equivalent dictionary, string, array, or data object by sending it a propertyList
message. For example, consider the following code sequence:
NSString *months = @"{
"january" = 31; "february" = 28; "march" = 31;
"april" = 30; "may" = 31; "june" = 30; "july" = 31;
"august" = 31; "september" = 30; "october" = 31;
"november" = 30; "december" = 31; }";
daysOfMonthDict = [months propertyList];
(Recall that to continue a long character string over several lines, a backslash character must appear as the very last character on the line. Also, to include a double quotation mark in a string, it must be escaped by preceding it with a backslash character.)
This sequence takes the dictionary encoded as a traditional property list in the string object months
and converts it to a dictionary object. To subsequently get the number of days in July, for example, you could then write the following:
[[daysOfMonthDict objectForKey: @"july"] intValue]
Obviously, this is a contrived example because there would be no advantage here of hard-coding a property list into your program. The dictionary could have been set up directly.
Incidentally, you can also write a property list to a URL and read one from a URL. For example, the statement
glossary = [NSDictionary dictionaryWithContentsOfURL:
[NSURL URLWithString:
@"http://www.kochan-wood.com/examples/glossary.pl"]];
creates a dictionary from the property list stored at the specified URL.
The dictionaryWithContentsOfURL:
method is not currently implemented under GNUStep. Another way to read the property list from a Web site is to use the stringWithContentsOfURL:
method to first read it into a string and then convert it to a property list using the propertyList
method. The can be done with a statement like this:
glossary = [[NSString stringWithContentsOfURL:
[NSURL URLWithString: @"http://www.kochan-wood.com/examples/glossary.pl"]
] propertyList];
Data can be written to a URL using the writeToURL:atomically:
method. For more information about working with URLs, consult the documentation for the Foundation class NSURL
and the header file <Foundation/NSURL.h>
.
NSArchiver
A more flexible approach allows for the saving of any types of objects to a file, not just strings, arrays, and dictionaries. Let's begin with the simple glossary created in Chapter 15. Program 19.3 shows how the glossary can be saved to a file on disk using the method archiveRootObject:toFile:
from the NSArchiver
class. To use this class, include the file
#import <Foundation/NSArchiver.h>
in your program.
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSArchiver.h>
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSDictionary *glossary =
[NSDictionary dictionaryWithObjectsAndKeys:
@"A class defined so other classes can inherit from it",
@"abstract class",
@"To implement all the methods defined in a protocol",
@"adopt",
@"Storing an object for later use",
@"archiving",
nil
];
[NSArchiver archiveRootObject: glossary toFile: @"glossary.archive"];
[pool release];
return 0;
}
Program 19.3 does not produce any output at the terminal. However, the statement
[NSArchiver archiveRootObject: glossary toFile: @"glossary.archive"];
writes the dictionary glossary
to the file glossary.archive
. Any pathname can be specified for the file. In this case, the file is written to the current directory.
The archive file created can later be read into an executing program by using NSUnarchiver
's unArchiveObjectWithFile:
method, as is done in Program 19.4.
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSArchiver.h>
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSDictionary *glossary;
NSEnumerator *keyEnum;
id key;
glossary = [NSUnarchiver unarchiveObjectWithFile:
@"glossary.archive"];
keyEnum = [glossary keyEnumerator];
while ( (key = [keyEnum nextObject]) != nil ) {
printf ("%s: %s
", [key cString],
[[glossary objectForKey: key] cString]);
}
[pool release];
return 0;
}
abstract class: A class defined so other classes can inherit from it.
adopt: To implement all the methods defined in a protocol
archiving: Storing an object for later use.
The statement
glossary = [NSUnarchiver unarchiveObjectWithFile:
@"glossary.archive"];
causes the specified file to be opened and its contents to be read. This file must be the result of a previous archive operation. You can specify a full pathname for the file or a relative path name, as was done in the example.
After the glossary has been restored, the program simply enumerates its contents to verify that the restore was successful.
Basic Objective-C class types such as NSString
, NSArray
, NSDictionary
, NSSet
, NSDate
, NSNumber
, and NSData
can be archived and restored in the manner just described. That includes nested objects as well, such as an array containing string or even other array objects.
This implies that you can't directly archive your AddressBook
using this method because the Objective-C system doesn't know how to archive an AddressBook
object. If you were to try to archive it by inserting a line such as
[NSArchiver archiveRootObject: myBook toFile: @"addrbook.arch"];
into your program, you'd get the following message displayed if you ran the program under Mac OS X:
2003-07-23 12:03:05.267 a.out[3516] *** -[AddressBook encodeWithCoder:]:
selector not recognized
2003-07-23 12:03:05.268 a.out[3516] *** Uncaught exception:
<NSInvalidArgumentException> *** -[AddressBook encodeWithCoder:]:
selector not recognized
a.out: received signal: Trace/BPT trap
From the error messages, you can see that the system was looking for a method called encodeWithCoder:
in the AddressBook
class, but you never defined such a method.
To archive objects other than those listed, you have to tell the system how to archive, or encode, your objects and also how to unarchive, or decode, them. This is done by adding encodeWithCoder:
and initWithCoder:
methods to your class definitions according to the <NSCoding>
protocol. For our address book example, you'd have to add these methods to both the AddressBook
and AddressCard
classes.
The encodeWithCoder:
method is invoked each time the archiver wants to encode an object from the specified class, and the method tells it how to do so. In a similar manner, the initWithCoder:
method is invoked each time an object from the specified class is to be decoded.
In general, the encoder method should specify how to archive each instance variable in the object you want to save. Luckily, you have help doing this. For the basic Objective-C classes described previously, you can use the encodeObject:
method. On the other hand, for basic Objective-C data types (such as integers and floats), you must use a slightly more involved method called encodeValueOfObjCType:at:
. The decoder method, initWithCoder:
, works in reverse: You use decodeObject
for decoding basic Objective-C classes and decodeValueOfObjCType:at:
for the basic data types.
Program 19.5 adds the two encoding and decoding methods to both the AddressCard
and AddressBook
classes.
Program 19.5. Addresscard.h
Interface File
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArchiver.h>
@interface AddressCard: NSObject <NSCoding, NSCopying>
{
NSString *name;
NSString *email;
}
-(void) setName: (NSString *) theName;
-(void) setEmail: (NSString *) theEmail;
-(void) setName: (NSString *) theName andEmail: (NSString *) theEmail;
-(NSString *) name;
-(NSString *) email;
-(NSComparisonResult) compareNames: (id) element;
-(void) print;
// Additional methods for NSCopying protocol
-(AddressCard *) copyWithZone: (NSZone *) zone;
-(void) retainName: (NSString *) theName andEmail: (NSString *) theEmail;
@end
Here are the two new methods for your AddressCard
class to be added to the implementation file:
-(void) encodeWithCoder: (NSCoder *) encoder
{
[encoder encodeObject: name];
[encoder encodeObject: email];
}
-(id) initWithCoder: (NSCoder *) decoder
{
name = [[decoder decodeObject] retain];
email = [[decoder decodeObject] retain];
return self;
}
The encoding method encodeWithCoder:
is passed an NSCoder
object as its argument. For each object you want to encode, you send a message to this object. In the case of your address book, you have two instance variables called name
and email
. Because these are both NSString
objects, you use the encodeObject:
method to encode each of them in turn. These two instance variables are then added to the archive. Note that encodeObject:
can be used for any object that has implemented a corresponding encodeWithCoder:
method in its class.
The decoding process works in reverse. The argument passed to initWithCoder:
is again an NSCoder
object. You don't need to worry about this argument; just remember that it's the one that gets the messages for each object you want to extract from the archive.
Because you've stored two objects in the archive with the encoding method, when decoding you must extract them in the same order in which they were added. First, you use the decodeObject
message to get your name
decoded, followed by a second message to get the email. You retain both instance variables to ensure that they still exist and are valid after the unarchiving process is completed. Note that the decoding method is expected to return itself.
Similarly to your AddressCard
class, you add encoding and decoding methods to your AddressBook
class. The only line you need to change in your interface file is the @interface
directive to declare that the AddressBook
class now conforms to the NSCoding
protocol. The change looks like this:
@interface AddressBook: NSObject <NSCoding, NSCopying>
Here are the method definitions for inclusion in the implementation file:
-(void) encodeWithCoder: (NSCoder *) encoder
{
[encoder encodeObject: bookName];
[encoder encodeObject: book];
}
-(id) initWithCoder: (NSCoder *) decoder
{
bookName = [[decoder decodeObject] retain];
book = [[decoder decodeObject] retain];
return self;
}
The test program is shown next as Program 19.6.
#import "AddressBook.h"
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSString *aName = @"Julia Kochan";
NSString *aEmail = @"[email protected]";
NSString *bName = @"Tony Iannino";
NSString *bEmail = @"[email protected]";
NSString *cName = @"Stephen Kochan";
NSString *cEmail = @"[email protected]";
NSString *dName = @"Jamie Baker";
NSString *dEmail = @"[email protected]";
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
AddressCard *card1 = [[AddressCard alloc] init];
AddressCard *card2 = [[AddressCard alloc] init];
AddressCard *card3 = [[AddressCard alloc] init];
AddressCard *card4 = [[AddressCard alloc] init];
AddressBook *myBook = [AddressBook alloc];
// First set up four address cards
[card1 setName: aName andEmail: aEmail];
[card2 setName: bName andEmail: bEmail];
[card3 setName: cName andEmail: cEmail];
[card4 setName: dName andEmail: dEmail];
myBook = [myBook initWithName: @"Steve's Address Book"];
// Add some cards to the address book
[myBook addCard: card1];
[myBook addCard: card2];
[myBook addCard: card3];
[myBook addCard: card4];
[myBook sort];
if ([NSArchiver archiveRootObject: myBook toFile: @"addrbook.arch"] == NO)
printf ("archiving failed
");
[card1 release];
[card2 release];
[card3 release];
[card4 release];
[myBook release];
[pool release];
return 0;
}
This program creates the address book and then archives it to the file addrbook.arch
. In the process of creating the archive file, realize that the encoding methods from both the AddressBook
and AddressCard
classes were invoked. You can add some printf
calls to these methods if you want proof.
Program 19.7 shows how you can read the archive into memory to set up the address book from a file.
#import "AddressBook.h"
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
AddressBook *myBook;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
myBook = [NSUnarchiver unarchiveObjectWithFile: @"addrbook.arch"];
[myBook list];
[pool release];
return 0;
}
======== Contents of: Steve's Address Book =========
Jamie Baker [email protected]
Julia Kochan [email protected]
Stephen Kochan [email protected]
Tony Iannino [email protected]
====================================================
In the process of unarchiving the address book, the decoding methods added to your two classes were automatically invoked. Notice how easily you can read the address book back into the program.
The encodeObject:
method works for built-in classes and classes for which you write your encoding and decoding methods according to the NSCoding
protocol. If your instance contains some basic data types, such as integers or floats, you'll need to know how to encode and decode them. Here's a simple definition for a class called Foo
that contains three instance variables—one is an NSString
, another is an int
, and the third is a float
. The class has one setter method, three getters, and two encoding/decoding methods to be used for archiving:
@interface Foo: NSObject <NSCoding>
{
NSString *strVal;
int intVal;
float floatVal;
}
-(void) setAll: (NSString *) ss iVal: (int) ii fVal: (float) ff;
-(NSString *) strVal;
-(int) intVal;
-(float) floatVal;
@end
The implementation file follows:
@implementation Foo;
-(void) setAll: (NSString *) ss iVal: (int) ii fVal: (float) ff
{
strVal = ss;
intVal = ii;
floatVal = ff;
}
-(NSString *) strVal { return strVal; }
-(int) intVal { return intVal; }
-(float) floatVal { return floatVal; }
-(void) encodeWithCoder: (NSCoder *) encoder
{
[encoder encodeObject: strVal];
[encoder encodeValueOfObjCType: @encode(int) at: &intVal];
[encoder encodeValueOfObjCType: @encode(float) at: &floatVal];
}
-(id) initWithCoder: (NSCoder *) decoder
{
strVal = [[decoder decodeObject] retain];
[decoder decodeValueOfObjCType: @encode(int) at: &intVal];
[decoder decodeValueOfObjCType: @encode(float) at: &floatVal];
return self;
}
@end
The encoding routine first encodes the string value strVal
using the encodeObject
method you used before. Next, you need to encode your integer and float fields. The method encodeValueOfObjCType:at:
takes two arguments to encode a basic Objective-C data type. The first is a special encoding obtained by applying the @encode
directive to the data type name. Because intVal
is an integer data type, you write @encode(int)
as the argument. The second argument is a pointer (as you encountered with the fileExistsAtPath:isDir:
method in Chapter 16, “Working with Files”) to the actual instance variable to be encoded and can be created by applying the address operator (&
) to the variable.
The floating variable floatVal
is encoded in a similar manner, by passing the arguments @encode(float)
and &floatVal
to the encodeValueOfObjCType:at:
method.
When decoding a basic data type, you use the decodeValueOfObjCType:at:
method, and the arguments are the same. In this case, the value decoded is stored at the memory address specified by the at:
argument.
You don't retain basic data types. They aren't objects, so they can't be retained.
In Program 19.8, a Foo
object is created, archived to a file, unarchived, and then displayed.
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArchiver.h>
#import <Foundation/NSAutoreleasePool.h>
#import "Foo.h" // Definition for our Foo class
int main (int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Foo *myFoo1 = [[Foo alloc] init];
Foo *myFoo2;
[myFoo1 setAll: @"This is the string" iVal: 12345 fVal: 98.6];
[NSArchiver archiveRootObject: myFoo1 toFile: @"foo.arch"];
myFoo2 = [NSUnarchiver unarchiveObjectWithFile: @"foo.arch"];
printf ("%s
%i
%g
", [[myFoo2 strVal] cString],
[myFoo2 intVal], [myFoo2 floatVal]);
[myFoo1 release];
[pool release];
return 0;
}
This is the string
12345
98.6
We noted in the discussion of the decoder methods that the fields in an archive must be read back in precisely the same order in which they were written. This technique might suit you; however, if you are creating archives from programs that might be going through many revisions, you might reorder some of the instance variables in one of your class definitions, or perhaps even add or remove some. In that case, restoring a previously created archive would be next to impossible.
A keyed archive is one in which each field of the archive has a name. When you archive an object, you give it a name, or key. When you retrieve it from the archive, you retrieve it by the same key. In that manner, objects can be written to the archive and retrieved in any order. Further, if new instance variables are added or removed to a class, the decoding method can account for it—for example, by setting a default value to a key that does not exist in the archive (for instance, if the archive were created by a different version of the program).
Instead of importing the file <Foundation/NSArchiver.h>
in your interface file, to work with keyed archives you need to import <Foundation/NSKeyedArchiver.h>
.
Referring to the Foo
class defined in the previous example, if you define a class that others will use, you don't really know whether they'll try to archive objects from your class using keyed archiving. To account for that, you can write your encoding and decoding methods to handle either keyed or unkeyed archives. This can be done by sending an allowsKeyedCoding
message to the encoder sent to your encodeWithCoder:
method. If the answer is YES
, you should use keyed archiving; otherwise, archive your objects in the manner described in the previous section. The same thing applies to your decoder: First, test whether keyed archiving is in effect and if it is, decode your instance variables accordingly.
Program 19.9 shows the modified Foo
class interface and implementation files to allow for keyed archiving.
Program 19.9. Foo
Interface File
@interface Foo: NSObject <NSCoding>
{
NSString *strVal;
int intVal;
float floatVal;
}
-(void) setAll: (NSString *) ss iVal: (int) ii fVal: (float) ff;
-(NSString *) strVal;
-(int) intVal;
-(float) floatVal;
@end
The interface file hasn't changed from the previous example, but the implementation file has.
Program 19.9. Foo
Implementation File
@implementation Foo;
-(void) setAll: (NSString *) ss iVal: (int) ii fVal: (float) ff
{
strVal = ss;
intVal = ii;
floatVal = ff;
}
-(NSString *) strVal { return strVal; }
-(int) intVal { return intVal; }
-(float) floatVal { return floatVal; }
-(void) encodeWithCoder: (NSCoder *) encoder
{
if ( [encoder allowsKeyedCoding] ) {
[encoder encodeObject: strVal forKey: @"FoostrVal"];
[encoder encodeInt: intVal forKey: @"FoointVal"];
[encoder encodeFloat: floatVal forKey: @"FoofloatVal"];
} else {
[encoder encodeObject: strVal];
[encoder encodeValueOfObjCType: @encode(int) at: &intVal];
[encoder encodeValueOfObjCType: @encode(float) at: &floatVal];
}
}
-(id) initWithCoder: (NSCoder *) decoder
{
if ( [decoder allowsKeyedCoding] ) {
strVal = [[decoder decodeObjectForKey: @"FoostrVal"] retain];
intVal = [decoder decodeIntForKey: @"FoointVal"];
floatVal = [decoder decodeFloatForKey: @"FoofloatVal"];
} else {
strVal = [[decoder decodeObject] retain];
[decoder decodeValueOfObjCType: @encode(int) at: &intVal];
[decoder decodeValueOfObjCType: @encode(float) at: &floatVal];
}
return self;
}
@end
After testing for keyed archiving, the three messages
[encoder encodeObject: strVal forKey: @"FoostrVal"];
[encoder encodeInt: intVal forKey: @"FoointVal"];
[encoder encodeFloat: floatVal forKey: @"FoofloatVal"];
archive the three instance variables from the object. The encodeObject:forKey:
method encodes an object and stores it under the specified key for later retrieval using that key. The key names are arbitrary, so as long you use the same name to retrieve the data as when you unarchived it, you can specify any key you like. The only time a conflict might arise is if the same key is used for a subclass of an object being encoded. To prevent this from happening, you can insert the class name in front of the instance variable name when composing the key for the archive, as was done in Program 19.9.
You use the method encodeInt:forKey:
instead of encodeValueOfObjCType:at:
, which you need to use for unkeyed archives. Table 19.1 depicts the various encoding and decoding methods you can use for keyed archives.
The process of decoding keyed objects is straightforward: You use decodeObject:forKey:
for Objective-C objects and the appropriate method from Table 19.1 for basic data types.
Table 19.1. Encoding and Decoding Basic Data Types in Keyed Archives
Some of the basic data types, such as char
, short
, long
, and long long
, are not listed in Table 19.1. You'll have to determine the size of your data object and use the appropriate routine. For example, a short int
is normally 16 bits, an int
and long
are 32 bits, and a long long
is 64 bits. (You can use the sizeof
operator as described in Chapter 13, “Underlying C Language Features,” to determine the size of any data type.) So, to archive a short int
, store it in an int
first and then archive it with encodeInt:forKey:
. Reverse the process to get it back: Use decodeInt:forKey:
and then assign it to your short int
variable.
The test program and output from the keyed archiving example is shown in Program 19.9.
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSCoder.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Foo *myFoo1 = [[Foo alloc] init];
Foo *myFoo2;
// First set and archive myFoo1 to a file
[myFoo1 setAll: @"This is the string" iVal: 12345 fVal: 98.6];
[NSKeyedArchiver archiveRootObject: myFoo1 toFile: @"foo.karch"];
// Now restore the archive into myFoo2
myFoo2 = [NSKeyedUnarchiver unarchiveObjectWithFile:
@"foo.karch"];
printf ("%s
%i
%g
", [[myFoo2 strVal] cString],
[myFoo2 intVal], [myFoo2 floatVal]);
[myFoo1 release];
[pool release];
return 0;
}
This is the string
12345
98.6
NSData
to Create Custom ArchivesYou might not want to write your object directly to a file using the archiveRootObject:ToFile:
method, as was done in the previous program examples. For example, perhaps you want to collect some or all of your objects and store them in a single archive file. This can be done in Objective-C using the general data stream object class called NSData
, which we briefly visited in Chapter 16.
As mentioned in Chapter 16, an NSData
object can be used to reserve an area of memory into which you can store data. Typical uses of this data area might be as temporary storage for data that will subsequently be written to a file or perhaps to hold the contents of a file read from the disk. The simplest way to create a mutable data area is with the data
method:
dataArea = [NSMutableData data];
This creates an empty buffer space whose size expands as needed as the program executes.
As a simple example, let's assume you want to archive your address book and one of your Foo
objects in the same file. Assume for this example that you've added keyed archiving methods to the AddressBook
and AddressCard
classes (see Program 19.10). If you haven't, or keyed archives aren't supported on your system, you can modify this example to work without keyed archives.
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSCoder.h>
#import <Foundation/NSData.h>
#import "AddressBook.h"
#import "Foo.h"
int main (int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
Foo *myFoo1 = [[Foo alloc] init];
Foo *myFoo2;
NSMutableData *dataArea;
NSKeyedArchiver *archiver;
AddressBook *myBook;
// Insert code from Program 19.6 to create an Address Book
// in myBook containing four address cards
[myFoo1 setAll: @"This is the string" iVal: 12345 fVal: 98.6];
// Set up a data area and connect it to an NSKeyedArchiver object
dataArea = [NSMutableData data];
archiver = [[NSKeyedArchiver alloc]
initForWritingWithMutableData: dataArea];
// Now we can begin to archive objects
[archiver encodeObject: myBook forKey: @"myaddrbook"];
[archiver encodeObject: myFoo1 forKey: @"myfoo1"];
[archiver finishEncoding];
// Write the archived data are to a file
if ( [dataArea writeToFile: @"myArchive" atomically: YES] == NO)
printf ("Archiving failed!
");
[archiver release];
[myFoo1 release];
[pool release];
return 0;
}
After allocating an NSKeyedArchiver
object, the initForWritingWithMutableData:
message is sent to specify the area in which to write the archived data; this is the NSMutabledata
area dataArea
you previously created. The NSKeyedArchiver
object stored in archiver
can now be sent encoding messages to archive objects in your program. In fact, all encoding messages up until it receives a finishEncoding
message are archived and stored in the specified data area.
You have two objects to encode here—the first is your address book and the second is your Foo
object. You can use encodeObject:
for these objects because you have previously implemented encoder and decode methods for the AddressBook
, AddressCard
, and Foo
classes. It's important that you understand that concept.
When you are done archiving your two objects, you send the archiver
object the finishEncoding
message. No more objects can be encoded after that point, and you need to send this message to complete the archiving process.
The area you set aside and named dataArea
now contains your archived objects in a form you can write to a file. The message expression
[data writeToFile: @"myArchive" atomically: YES]
sends the writeToFile:atomically:
message to your data stream to ask it to write its data to the specified file, which you named myArchive
.
As you can see from the if
statement, the writeToFile:atomically:
method returns a BOOL
value: YES
if the write operation succeeds and NO
if it fails (perhaps an invalid pathname for the file was specified or the file system is full).
Restoring the data from your archive file is simple—you just do things in reverse. First, you need to allocate a data area like before. Next, you need to read your archive file into the data area, and then you have to create an NSKeyedUnarchiver
object and tell it to decode data from the specified area. You must invoke decode methods to extract and decode your archived objects. When you're all done, you send a finishDecoding
message to the NSKeyedUnarchiver
object.
This is all done in Program 19.11 that follows.
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSCoder.h>
#import <Foundation/NSData.h>
#import "AddressBook.h"
#import "Foo.h"
int main (int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSData *dataArea;
NSKeyedUnarchiver *unarchiver;
Foo *myFoo1;
AddressBook *myBook;
// Read in the archive and connect an
// NSKeyedUnarchiver object to it
dataArea = [NSData dataWithContentsOfFile: @"myArchive"];
unarchiver = [[NSKeyedUnarchiver alloc]
initForReadingWithData: dataArea];
// Decode the objects we previously stored in the archive
myBook = [unarchiver decodeObjectForKey: @"myaddrbook"];
myFoo1 = [unarchiver decodeObjectForKey: @"myfoo1"];
[unarchiver finishDecoding];
[unarchiver release];
// Verify that the restore was successful
[myBook list];
printf ("%s
%i
%g
", [[myFoo1 strVal] cString],
[myFoo1 intVal], [myFoo1 floatVal]);
[pool release];
return 0;
}
======== Contents of: Steve's Address Book =========
Jamie Baker [email protected]
Julia Kochan [email protected]
Stephen Kochan [email protected]
Tony Iannino [email protected]
====================================================
This is the string
12345
98.6
The output verifies that the address book and your Foo
object were successfully restored from the archive file.
In Program 19.2, you tried to make a copy of an array containing mutable string elements and saw how a shallow copy of the array was made. That is, the actual strings themselves were not copied, only the references to them.
You can use the Foundation's archiving capabilities to create a deep copy of an object. For example, Program 19.12 copies dataArray
to dataArray2
by archiving dataArray
into a buffer and then unarchiving it, assigning the result to dataArray2
. You don't need to use a file for this process; the archiving and unarchiving process can all take place in memory.
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArchiver.h>
#import <Foundation/NSArray.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSData *data;
NSMutableArray *dataArray = [NSMutableArray arrayWithObjects:
[NSMutableString stringWithString: @"one"],
[NSMutableString stringWithString: @"two"],
[NSMutableString stringWithString: @"three"],
nil
];
NSMutableArray *dataArray2;
NSMutableString *mStr;
int i, n;
// Make a deep copy using the archiver
data = [NSArchiver archivedDataWithRootObject: dataArray];
dataArray2 = [NSUnarchiver unarchiveObjectWithData: data];
mStr = [dataArray2 objectAtIndex: 0];
[mStr appendString: @"ONE"];
printf ("
dataArray: ");
for (i = 0; i < [dataArray count]; ++i)
printf ("%s ", [[dataArray objectAtIndex: i] cString]);
printf ("
dataArray2: ");
n = [dataArray2 count];
for (i = 0; i < n; ++i)
printf ("%s ", [[dataArray2 objectAtIndex: i] cString]);
printf ("
");
[pool release];
return 0;
}
dataArray: one two three
dataArray2: oneONE two three
The output verifies that changing the first element of dataArray2
had no effect on the first element of dataArray
. That's because a new copy of the strings was made through the archiving/unarchiving process.
The copy operation in Program 19.12 is performed with the following two lines:
data = [NSArchiver archivedDataWithRootObject: dataArray];
dataArray2 = [NSUnarchiver unarchiveObjectWithData: data];
You can even avoid the intermediate assignment and perform the copy with a single statement like this:
dataArray2 = [NSUnarchiver unarchiveObjectWithData:
[NSArchiver archivedDataWithRootObject: dataArray]];
This is a technique you might want to keep in mind next time you need to make a deep copy of an object or of an object that doesn't support the NSCopying
protocol.
primes.pl
. Then, examine the contents of the file.$ glossary object
should display the meaning of “object” as defined in the glossary. Be sure to handle the cases where a term can't be found or if the term consists of more than one word, like this:
$ glossary instance method
This should look up the meaning of the term “instance method” in the glossary.
AddressBook
and look up an entry based on a name supplied on the command line, like so:
$ lookup gregory
3.137.221.120