天天看點

Brief Intro to Archives and Serialization of Foundation Framework

Archives and Serialization

The Foundation Framework archives and serialization classes implement mechanisms for converting an object (i.e., an object graph) into an architecture-independent byte buffer. This data can then be written to a file or transmitted to another process, potentially over a network. Later, the data can be converted back into objects, preserving the associated object graph. In this way, these classes provide a lightweight means of data persistence. The serialization process preserves the data and the positions of objects in an object hierarchy, whereas the archiving process is more general purpose—it preserves the data, data types, and the relations between the objects in an object hierarchy.

Archiving

NSCoder is an abstract class that declares the interface used to bothmarshal and unmarshall object graphs. The marshalling process converts an object’s information into a series of bytes, and the unmarshalling process creates an object from a (previously marshalled) series of bytes.NSCoder includes methods for encoding and decoding data of various types, testing an NSCoder instance, and provides support for secure coding. The Foundation Framework includes four concrete subclasses ofNSCoder: NSArchiver, NSUnarchiver, NSKeyedArchiver, andNSKeyedUnarchiver.

Sequential Archives

NSArchiver and NSUnarchiver are used to create sequential archives, which means that the objects and values of a sequential archive must be decoded in the same order that they were encoded. In addition, when decoding a sequential archive, the entire object graph must be decoded.NSArchiver is used to encode objects for writing to a file or some other use, and NSUnarchiver is used to decode objects from an archive.NSArchiver includes methods for initialization, archiving data, retrieving archived data, and substituting classes or objects in an archive.NSUnarchiver includes methods for initialization, decoding objects, substituting classes or objects, and management.

Keyed Archives

Whereas NSArchiver and NSUnarchiver are sequential archives,NSKeyedArchiver and NSKeyedUnarchiver are keyed archives—each value in the archive can be individually named/keyed. The key must be unique within the scope of the object in which the value is being encoded/decoded. When decoding a keyed archive, the values can be decoded out of sequence or not at all. Hence, keyed archives provide better support for forward and backward compatibility and are recommended over the sequential archiving classes. NSKeyedArchiverincludes methods for initialization, archiving data, encoding data and objects, and management. NSKeyedUnarchiver includes methods for initialization, unarchiving data, decoding objects, and management.

The code shown in Listing 12-5 uses the NSKeyedArchiverarchiveRootObject: method to archive an NSString instance namedgreeting to a file in the current directory named greeting.archive.

Listing 12-5.  Archiving an Object with NSKeyedArchiver

NSString *greeting = @"Hello, World!";
NSString *cwd = [[NSFileManager defaultManager] currentDirectoryPath];
NSString *archivePath = [cwd stringByAppendingString:@"/greeting.archive"];
BOOL result = [NSKeyedArchiver archiveRootObject:greeting toFile:archivePath];      

The next code fragment uses the NSKeyedUnarchiverunarchiveObjectWithFile: method to decode an NSString object named greeting from an archive stored in the file archivePath.

NSString *greeting = [NSKeyedUnarchiver unarchiveObjectWithFile:archivePath];      

Encoding and Decoding Objects

While the NSKeyedArchiver and NSKeyedUnarchiver classes are responsible for the archiving process, a class must conform to theNSCoding protocol to support enconding/decoding of class instances. This protocol declares two methods, encodeWithCoder: and initWithCoder:, that encode/decode an object’s state (e.g., its properties and instance variables). When a coder object (i.e., an NSKeyedArchiver orNSKeyedUnarchiver instance) archives an object, it instructs the object to encode its state by invoking its encodeWithCoder: method. Hence, a class must implement the appropriate encode and decode method(s) because these will be called by the selected coder object. Listing 12-6depicts an implementation of a class named MyType that conforms to theNSCoding protocol.

Listing 12-6.  Implementing the NSCoding Protocol Methods

@interface MyType : NSObject <NSCoding>

@property NSString *type;

@end

@implementation MyType

- (void)encodeWithCoder:(NSCoder *)coder
{
  [coder encodeObject:self.type forKey:@"TYPE_KEY"];
}

- (id)initWithCoder:(NSCoder *)coder
{
  if ((self = [super init]))
  {
    type = [coder decodeObjectForKey:@"TYPE_KEY"];
  }
  return self;
}

@end      

Property List Serialization

Property list serialization provides a means of converting a property list, a structured collection of data organized as name-value pairs, to/from an architecture-independent byte stream. The Foundation FrameworkNSPropertyListSerialization class provides methods to serialize and deserialize property lists directly and validate a property list. It also supports conversion of property lists to/from XML or an optimized binary format. In contrast to archiving, basic serialization does not record the data type of the values nor the relationships between them; only the values themselves are recorded. Hence, you must write your code to deserialize data with the proper types and in the proper order.

A property list organizes data as named values and collections of values. They are frequently used to store, organize, and access standard types of data. Property lists can be created programmatically, or more commonly, as XML files.

XML Property Lists

Property lists are most commonly stored in XML files, referred to as XML plist files. The NSArray and NSDictionary classes both have methods to persist themselves as XML property list files and to create class instances from XML plists.

NSPropertyListSerialization

The NSPropertyListSerialization class enables the programmatic creation of property lists. This class supports the following Foundation data types (the corresponding Core Foundation toll-free bridged data types are provided in parentheses):

  • NSData (CFData)
  • NSDate (CFDate)
  • NSNumber: integer, float, and Boolean values (CFNumber,CFBoolean)
  • NSString (CFString)
  • NSArray (CFArray)
  • NSDictionary (CFDictionary)

Because the supported data types include the collection classes NSArrayand NSDictionary, each of which can contain other collections, anNSPropertyListSerialization object can be used to create hierarchies of data. As a property list is structured as a collection of name-value pairs, a dictionary is used to programmatically create property list data.Listing 12-7 demonstrates the use of the instance methoddataWithPropertyList:format:options:error: to serialize anNSDictionary property list collection of name-value pairs to a data buffer named plistData.

Listing 12-7.  Property List Serialization of Name-Value Pairs

NSError *errorStr;
NSDictionary *data = @{ @"FirstName" : @"John", @"LastName" : @"Doe" };
NSData *plistData = [NSPropertyListSerialization dataWithPropertyList:data
                     format:NSPropertyListXMLFormat_v1_0
                     options:0
                     error:&errorStr];      

The format: parameter specifies the property list format, of type enumNSPropertyListFormat. The allowed values are as follows:

  • NSPropertyListOpenStepFormat: Legacy ASCII property list format.
  • NSPropertyListXMLFormat_v1_0: XML property list format.
  • NSPropertyListBinaryFormat_v1_0: Binary property list format.

The options: parameter is meant to specify the selected property list write option. This parameter is currently unused and should be set to 0. If the method does not complete successfully, an NSError object is returned in the error: parameter that describes the error condition.Listing 12-8 demonstrates use of thepropertyListWithData:options:format:error: method to deserialize a property list from the plistData data buffer of Listing 12-7.

Listing 12-8.  Property List Deserialization

NSError *errorStr;
NSDictionary *plist = [NSPropertyListSerialization
                       propertyListWithData:plistData
                       options:NSPropertyListImmutable
                       format:NULL
                       error:&errorStr];      

The options: parameter specifies the property list read option. This value can be any of those for the enum typeNSPropertyListMutabilityOptions. The possible values are as follows:

  • NSPropertyListImmutable: The returned property list contains immutable objects.
  • NSPropertyListMutableContainers: The returned property list has mutable containers but immutable leaves.
  • NSPropertyListMutableContainersAndLeaves: The returned property list has mutable containers and mutable leaves.

The format: parameter contains the format that the property list was stored in. If the value NULL is provided, then it is not necessary to know the format. The possible non-NULL values for the format are of enum type NSPropertyListFormat.

Property list serialization does not preserve the full class identity of its objects, only its general kind. In other words, a property list object may be any of the preceding supported types. When a collection class is stored as a property list, its elements must also be in the list of supported property list data types. In addition, the keys forNSDictionary property list objects must be of type string (NSString). As a result, if a property list is serialized and then deserialized, the objects in the resulting property list might not be of the same class as the objects in the original property list. In particular, when a property list is serialized, the mutability of the container objects (i.e., NSDictionaryand NSArray objects) is not preserved. When deserializing, though, you can choose to have all container objects created mutable or immutable.

Property list serialization also does not track the presence of objects referenced multiple times. Each reference to an object within the property list is serialized separately, resulting in multiple instances when deserialized.

Archiving an Object Graph

OK, now that you have a good handle on the archiving and serializationclasses, you’re going to create a program that demonstrates use of theFoundation Framework Archiving APIs. This program will create an object graph from a class hierarchy and then encode and decode the object graph from an archive. The classes that you’ll develop are diagrammed in Figure 12-1.

Brief Intro to Archives and Serialization of Foundation Framework

Figure 12-1. ArchiveCat program class hierarchy

As shown in Figure 12-1, the program consists of a class hierarchy (theSubfamily-Family-Order classes) and a class (Archiver) that’s used to archive instances of this hierarchy. In Xcode, create a new project by selecting New 

Brief Intro to Archives and Serialization of Foundation Framework

 Project . . . from the Xcode File menu. In the New Project Assistant pane, create a command-line application. In theProject Options window, specify ArchiveCat for the Product Name, choose Foundation for the Project Type, and select ARC memory management by selecting the Use Automatic Reference Countingcheck box. Specify the location in your file system where you want the project to be created (if necessary, select New Folder and enter the name and location for the folder), uncheck the Source Control check box, and then click the Create button.

Next, you’re going to create the class hierarchy for the object graph. You’ll start with the base class and then successively implement the remaining subclasses. Select New 

Brief Intro to Archives and Serialization of Foundation Framework

 File . . . from the Xcode File menu, select the Objective-C class template, and name the class Order. Select the ArchiveCat folder for the files location and the ArchiveCat project as the target, and then click the Create button. Next, in the Xcode project navigator pane, select the resulting header file named Order.hand update the interface, as shown in Listing 12-9.

Listing 12-9.  Order Interface

#import <Foundation/Foundation.h>

@interface Order : NSObject <NSCoding>

@property (readonly) NSString *order;

- (id)initWithOrder:(NSString *)order;

@end      

The Order interface adopts the NSCoding protocol, as required for classes that support archiving. The read-only property order identifies the order group in biological classification. The initWithOrder: method initializes an Order object, setting the property to the input parameter value. Now select the Order.m file and update the implementation, as shown in Listing 12-10.

Listing 12-10.  Order Implementation

#import "Order.h"

@implementation Order

- (id)initWithOrder:(NSString *)order
{
  if ((self = [super init]))
  {
    _order = order;
  }
   
  return self;
}

- (id)initWithCoder:(NSCoder *)coder
{
  if ((self = [super init]))
  {
    _order = [coder decodeObjectForKey:@"ORDER_KEY"];
  }
  return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{
  [coder encodeObject:self.order forKey:@"ORDER_KEY"];
}

- (NSString *) description
{
  return [NSString stringWithFormat:@"Order:%@", self.order];
}

@end      

The initWithOrder: implementation is very similar to init methods you’ve developed elsewhere in this book. It simply assigns the orderinput parameter to the order property’s backing instance variable.

The initWithCoder: method, declared by the NSCoding protocol, initializes the object using the archived state. Its input parameter, coder, is the NSCoder instance used to decode the Order instance archive. The superclass of Order is NSObject; because NSObject doesn’t adopt theNSCoding protocol, the self variable is assigned the returned value of the superclass init call.

self = [super init]      

Next, the Order class state (represented by its properties and instance variables) is decoded and initialized. As the Order class has a single property named order, the property’s instance variable is assigned to the value decoded by the decodeObjectForKey: method, where the key is named ORDER_KEY.

The encodeWithCoder: method is used to archive the Order class state, its input parameter, coder, is the NSCoder instance used to encode theOrder instance archive. Because the superclass of Order doesn’t adopt the NSCoding protocol, this method doesn’t invoke the superclass’sencodeWithCoder: method, but just encodes the Order class state.Specifically, the method invokes the encodeWithCoder: method on the coder for each property/variable that needs to be archived.

[coder encodeObject:self.order forKey:@"ORDER_KEY"];      

Finally, the class overrides the description method (inherited from its superclass) to return a text string listing the value for the order property.

Now you’ll implement the next class in the hierarchy. Select New 

Brief Intro to Archives and Serialization of Foundation Framework

 File . . . from the Xcode File menu, select the Objective-C class template, and name the class Family. Select the ArchiveCat folder for the files location and the ArchiveCat project as the target, and then click theCreate button. Next, in the Xcode project navigator pane, select the resulting header file named Family.h and update the interface, as shown in Listing 12-11.

Listing 12-11.  Family Interface

#import "Order.h"

@interface Family : Order

@property(readonly) NSString *family;

- (id)initWithFamily:(NSString *)family order:(NSString *)order;

@end      

The Family interface subclasses the Order class, and hence adopts theNSCoding protocol. The read-only property family specifies the family group in a biological classification. The initWithFamily:order: method initializes a Family object, setting the family and order properties to the input parameter values provided. Now select the Family.m file and update the implementation, as shown in Listing 12-12.

Listing 12-12.  Family Implementation

#import "Family.h"

@implementation Family

- (id)initWithFamily:(NSString *)family order:(NSString *)order
{
  if ((self = [super initWithOrder:order]))
  {
    _family = family;
  }
   
  return self;
}

- (id)initWithCoder:(NSCoder *)coder
{
  if ((self = [super initWithCoder:coder]))
  {
    _family = [coder decodeObjectForKey:@"FAMILY_KEY"];
  }
  return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{
  [super encodeWithCoder:coder];
  [coder encodeObject:self.family forKey:@"FAMILY_KEY"];
}

- (NSString *) description
{
  return [NSString stringWithFormat:@"Family:%@, %@", self.family,
          [super description]];
}

@end      

This implementation is very similar to that of the Order class, so you’ll just focus on the key differences. The initWithFamily:order: invokes the superclass initWithOrder: method to initialize the superclass state properly, and then assigns the family input parameter to the property’s backing instance variable.

The initWithCoder: method is very similar to that provided for theOrder class (as shown in Listing 12-10). However, as the superclass of the Family class (Order) adopts the NSCoding protocol, the self variable is assigned the returned value of the superclass initWithCoder: call.

self = [super initWithCoder:coder]      

In this way, the superclass state (the order property) is initialized properly. Next, the Family class state (represented by its properties and instance variables) is decoded and initialized. As the Family class has a single property named family, the property’s instance variable is assigned to the value decoded by the coder’s decodeObjectForKey:method, where the key is named FAMILY_KEY.

The encodeWithCoder: method is used to archive the Family class state. Because the superclass of Family (the Order class) adopts the NSCodingprotocol, this method first invokes invoke the superclass’sencodeWithCoder: method. Next, it invokes the encodeWithCoder:method on the coder for each property/variable that needs to be archived; in this case, the family property.

As with the Order class, the description method returns a text string consisting of the value of the family property concatenated with the value of the description for its superclass.

return [NSString stringWithFormat:@"Family:%@, %@", self.family,
        [super description]];      

Now you’ll implement the final class in the hierarchy. Select New 

Brief Intro to Archives and Serialization of Foundation Framework

 File . . . from the Xcode File menu, select the Objective-C class template, and name the class Subfamily. Select the ArchiveCat folder for the files location and the ArchiveCat project as the target, and then click theCreate button. Next, in the Xcode project navigator pane, select the resulting header file named Subfamily.h and update the interface, as shown in Listing 12-13.

Listing 12-13.  Subfamily Interface

#import "Family.h"

@interface Subfamily : Family

@property(readonly) NSString *genus;
@property(readonly) NSString *species;

- (id)initWithSpecies:(NSString *)species
             genus:(NSString *)genus
            family:(NSString *)family
             order:(NSString *)order;

@end      

The Subfamily interface subclasses the Family class. The read-only properties genus and species specifies the genus and species for an animal group in a biological classification. TheinitWithSpecies:family:order: method initializes a Subfamily object, similar to the corresponding methods for the Family and Order classes. Now select the Subfamily.m file and update the implementation, as shown in Listing 12-14.

Listing 12-14.  Subfamily Implementation

#import "Subfamily.h"

@implementation Subfamily

- (id)initWithSpecies:(NSString *)species
             genus:(NSString *)genus
            family:(NSString *)family
             order:(NSString *)order
{
  if ((self = [super initWithFamily:family order:order]))
  {
    _species = species;
    _genus = genus;
  }
   
  return self;
}

- (id)initWithCoder:(NSCoder *)coder
{
  if ((self = [super initWithCoder:coder]))
  {
    _species = [coder decodeObjectForKey:@"SPECIES_KEY"];
    _genus = [coder decodeObjectForKey:@"GENUS_KEY"];
  }
  return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{
  [super encodeWithCoder:coder];
  [coder encodeObject:self.species forKey:@"SPECIES_KEY"];
  [coder encodeObject:self.genus forKey:@"GENUS_KEY"];
}

- (NSString *) description
{
  return [NSString stringWithFormat:@"Animal - Species:%@ Genus:%@, %@",
          self.species, self.genus, [super description]];
}

@end      

This implementation is very similar to that of the Family class, differing primarily in the Subfamily class state (the genus and speciesproperties). In all other respects, the logic is identical, as you’ll see if you compare Listing 12-12 and Listing 12-14. Now you’ll implement the class used to archive the hierarchy. Select New 

Brief Intro to Archives and Serialization of Foundation Framework

 File . . . from the Xcode File menu, select the Objective-C class template, and name the classArchiver. Select the ArchiveCat folder for the files location and theArchiveCat project as the target, and then click the Create button. Next, in the Xcode project navigator pane, select the resulting header file named Archiver.h and update the interface, as shown in Listing 12-15.

Listing 12-15.  Archiver Interface

#import <Foundation/Foundation.h>

@interface Archiver : NSObject

@property (readwrite) NSString *path;

- (BOOL) encodeArchive:(id)data toFile:(NSString *)file;
- (id) decodeArchiveFromFile:(NSString *) file;

@end      

The Archiver interface has a single property, path, which defines the path for the file the archive is written to. The methodsencodeArchive:toFile: and decodeArchiveFromFile: are used to encode/decode an archive to/from a file on the file system. Now select the Archiver.m file and update the implementation, as shown in Listing 12-16.

Listing 12-16.  Archiver Implementation

#import "Archiver.h"

@implementation Archiver

- (id) init
{
  if ((self = [super init]))
  {
    _path = NSTemporaryDirectory();
  }
   
  return self;
}

- (BOOL) encodeArchive:(id)objectGraph toFile:(NSString *)file
{
  NSString *archivePath = [self.path stringByAppendingPathComponent:file];
   
  // Create an archiver for encoding data
  NSMutableData *mdata = [[NSMutableData alloc] init];
  NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
                               initForWritingWithMutableData:mdata];
   
  // Encode the data, keyed with a simple string
  [archiver encodeObject:objectGraph forKey:@"FELINE_KEY"];
  [archiver finishEncoding];
   
  // Write the encoded data to a file, returning status of the write
  BOOL result = [mdata writeToFile:archivePath atomically:YES];
  return result;
}

- (id) decodeArchiveFromFile:(NSString *) file
{
  // Get path to file with archive
  NSString *archivePath = [self.path stringByAppendingPathComponent:file];
   
  // Create an unarchiver for decoding data
  NSData *data = [[NSMutableData alloc] initWithContentsOfFile:archivePath];
  NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]
                                   initForReadingWithData:data];
   
  // Decode the data, keyed with simple string
  id result = [unarchiver decodeObjectForKey:@"FELINE_KEY"];
  [unarchiver finishDecoding];
   
  // Return the decoded data
  return result;
}

@end      

As shown in Listing 12-16, the init method sets the value for the path property. It uses the Foundation NSTemporaryDirectory() function to create a path to the user’s temporary directory on the file system, and assigns that value to the property’s backing instance variable.

The encodeArchive:toFile: method encodes an object graph to a file. It creates a file path by prepending the path property to the input file string. It then creates a mutable data object for archiving the graph. Next, it creates an NSKeyArchiver instance to perform the archiving, initialized with the data object. It encodes the graph to the data object with the key FELINE_KEY, and then finishes the encoding. Finally, it writes the archived data object to the file, returning a Boolean that indicates the success/failure of the write.

The decodeArchiveFromFile: method decodes an archive from a file, returning the initialized object graph. It creates a file path by prepending the path property to the input file string. It then creates a data object for unarchiving the graph. Next, it creates an NSKeyUnarchiver instance to perform the unarchiving, initialized with the data object. It decodes the graph to a data object with the key FELINE_KEY, finishes the decoding, and then returns the initialized data object.

And that’s it! Now that you have implemented the class hierarchy and the archiver class, let’s use this to archive an object graph. In the Xcode project navigator, select the main.m file and update the main()function, as shown in Listing 12-17.

Listing 12-17.  ArchiveCat main( ) Function

#import <Foundation/Foundation.h>
#import "Archiver.h"
#import "Subfamily.h"

int main(int argc, const char * argv[])
{
  @autoreleasepool
  {
    // Create an Archiver to encode/decode an object graph
    Archiver *archiver = [[Archiver alloc] init];
     
    // Create an object graph and archive it to a file
    id animal = [[Subfamily alloc] initWithSpecies:@"Lion"
                                             genus:@"Panther"
                                            family:@"Felid"
                                             order:@"Carnivore"];
    NSLog(@"\n%@", [animal description]);
    NSString *file = @"data.archive";
     
    // Display results
    if ([archiver encodeArchive:animal toFile:file])
    {
      NSLog(@"You encoded an archive to file %@",
            [[archiver path] stringByAppendingString:file]);
    }
     
    // Decode object graph from archive and log its description
    id data = [archiver decodeArchiveFromFile:file];
    if ([archiver decodeArchiveFromFile:file])
    {
      NSLog(@"You decoded an archive from file %@\n%@",
            [[archiver path] stringByAppendingString:file], [data description]);
    }
     
  }
  return 0;
}      

As shown in Listing 12-17, the main() function begins by creating anArchiver object. It then creates an object graph, logs its description to the output pane, and names the archive file. Next, the graph is archived to the named archive file, and if successful, a message is logged to the console.

The next set of statements decodes an object graph from the archive. First, it decodes the archive using the Archiver decodeArchiveFromFile:method. It then performs a condition check on the result of the method call, and if it returns an object graph (meaning the operation completed successfully), it logs a description of the object graph to the output pane. Now when you compile and run the ArchiveCat program, you should observe messages in the output pane comparable to those shown inFigure 12-2.

Brief Intro to Archives and Serialization of Foundation Framework

Figure 12-2. ArchiveCat program output

As shown in the output pane, a Subfamily object (i.e., object graph) is created and initialized with its input parameters, and its description is logged to the output pane. Next, the object is archived to the specified file, and the full path of the archive file is logged to the output pane. A corresponding object is then decoded from the archive and its description is logged to the output pane. As the description for the initially created object and the object decoded from the archive is identical, this validates that the archive was correctly encoded and decoded. This demonstrates use of the Archiving APIs to encode/decode an archive. Look this code over for a while and make sure that you have a good handle on the archiving process. When you’re ready, let’s move on to distributed objects.