Objective-C 学习笔记 - 第13章 归档和序列化

归档(archiving)将对象与数值储存至架构独立(architecture-independent)的位串流,它能储存至文件待日后复原,也能直接传送给另一个程序使用,进而让对象与数值的关系在任何程序中,都具备一致性。归档让相关的对象与键值的集合,能以更详细的方式记录。

一、Property LIst 结构表示方法

Property List(通称为plist)是一个对象间结构的表示方法。在 Mac OS X 的系统中,应用程序能将 property lists 输出为二进制(binary)格式或 XML 格式。plist 具备轻易建立与修改的特性,用户仅需使用文本编辑器,或在 Mac OS X 开发工具中的 Property List Editor 应用程序(位于 Macintosh HD/Deveploer/Application/Utilities 目录)进行编写即可。Foundation Framework 的集合对象 NSArray、NSDictionary,两者都有方法能直接将内容以 XML plist 的格式记录于文件。

范例程序 archive_property_list_write
// archive_property_list_write
#import 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 文件储存路径
        NSString *path = [NSHomeDirectory() 
                          stringByAppendingPathComponent:@"dict.plist"];
        // 建立 dictionary,放入 key-pair 键值
        NSDictionary *dict = [[NSDictionary alloc] initWithObjectsAndKeys:
                          @"printf()", @"C",
                          @"cout<<", @"C++",
                          @"NSLog()", @"Objective-C",
                          @"System.out.print()", @"Java", nil];
        // 将 dictionary 写入文件 path
        if (YES == [dict writeToFile:path atomically:YES])
        {
        NSLog(@"文件写入成功");
        }
    }
    return 0;
}

程序先以 stringByAppendingPathComponent: 方法,定义 NSDictionary 的储存文件的路径。之后以 writeToFile:atomically: 方法,将数据写入文件中。请注意键值必须是 NSString 类型,才能成功写入文件。以下为输出的文件内容:

输出结果




    C
    printf()
    C++
    cout<<
    Java
    System.out.print()
    Objective-C
    NSLog()


能看见这是一个标准的 XML 格式文件,除了键值 必须为 NSString 类型之外,此键值对应的对象值可以如表1所列的数据类型:

数据类型 XML 元素 Foundation framework 类别
array NSArray
dictionary NSDictionary
string NSString
data NSData
date NSDate
number-integer NSNumber(intValue)
number-floating point NSNumber(intFloat)
Boolean NSNumber(boolValue)

表1中的数据类型,称为 plist 对象(property list object)类型,也就是 plist 结构所支持的对象类型。范例程序 archive_property_list_read 能将 XML plist 文件还原至程序。

范例程序 archive_property_list_read
// archive_property_list_read
#import 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 文件储存路径
        NSString *path = [NSHomeDirectory()
                      stringByAppendingPathComponent:@"dict.plist"];
        // 将文件读取至 recovery,然后显示出来
        NSDictionary * recovery = [NSDictionary dictionaryWithContentsOfFile:path];
        for (NSString *key in recovery) {
            NSLog(@"key:%@, value:%@", key, [recovery objectForKey:key]);
        }
    }
    return 0;
}
输出结果
key:Java, value:System.out.print()
key:Objective-C, value:NSLog()
key:C, value:printf()
key:C++, value:cout<<   

程序由原路径将 XML plist 文件,利用 dictionaryWithContentsOfFile: 方法将数据读取,并指定给变量 recover,若这里将 NSDictionary 类别改成 NSArray,是一个错误的操作方式,对象依然能产生,但会返回 NULL 不会有任何内容,请特别注意。

二、NSKeyedArchiver类别

对象图形(object graphs)是指对象与对象间关系图。相较于上述介绍的对象,archiving 能处理较复杂的对象图形,以及更多 plist 结构不支持的对象,因而突显它的重要性。

NSCoder 是所有 archiving 对象的抽象父类别,它定义一些 archive 的必要方法,Foundation framework 底下有5种类别继承自它,如表2所示:

Archiving 特色 类别名称 功能
顺序归档 Sequential archive NSArchiver 以顺序的方式进行编码
顺序归档 Sequential archive NSUnarchiver 以顺序的方式进行编码
键值归档 Keyed archive NSKeyedArchiver 以键值组的方式进行编码
键值归档 Keyed archive NSKeyedUnarchiver 以键值组的方式进行编码
分布式对象 Distributed objects NSPortCoder 程序或线程之间交换数据的方式

由于顺序归档已于 Mac OS X 10.2 退役,而分布式对象在一般程序中较少使用,因此本节着重于运用最广泛的 NSKeyedArchiver,它也是在 10.2 时出现的功能。范例程序 archive_nsKeyedArchiver_write 示范如何使用 NSKeyedArchiver 类别,将对象储存为文件。

范例程序 archive_nsKeyedArchiver_write
// archive_nsKeyedArchiver_write
#import 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 文件储存路径
        NSString *path = [NSHomeDirectory()
                      stringByAppendingPathComponent:@"array.plist"];
        // 创建一个数组
        NSArray *array = [NSArray arrayWithObjects:@"one", @"two", @"three", nil];
        // 写入文件
        if (YES == [NSKeyedArchiver archiveRootObject:array toFile:path]) {
            NSLog(@"文件写入成功");
        }
    }
    return 0;
}

若文件 array.plist 成功建立,会输出“文件写入成功”的提示信息。由上述程序发现,以 NSKeyedArchiver 的类别方法 rchiveRootObject:toFile: 将数组 array 以 property list 的格式存入路径 path 指定的文件。

范例程序 archive_nsKeyedArchiver_read 将刚才储存至 array.plist 的内容,还原至目前程序中。

范例程序 archive_nsKeyedArchiver_read
// archive_nsKeyedArchiver_read
#import 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 文件储存路径
        NSString *path = [NSHomeDirectory()
                      stringByAppendingPathComponent:@"array.plist"];
        // 从文件读取数组
        NSArray *recovery = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
        // 显示数组
        for (NSString *strTemp in recovery) {
            NSLog(@"%@",strTemp);
        }
    }
    return 0;
}

unarchiveObjectWithFile: 也是NSKeyedUnarchiver 的类别方法,只需要传入文件路径 path,即可取得复原的内容。请注意,返回值为 nil 代表文件不存在,若文件不是有效的 archive 内容,则会出现 NSInvalidArgumentException 的异常。

三、自定义类别的 archive

上述谈了关于 archive 的内容,也由表格了解了 archive 支持的基本数据类型,接着我们将探讨的主题是自定义类。若仔细查看官方的技术文件,能发现几乎所有 Foundation framework 的类别,都遵循 NSCoding 协议,并实现 encodeWithCoder: 以及 initWithCoder: 这二个必要的方法,它们都有一个 NSCoder 类型的参数,前者的参数能将 archive 的对象编码为串流,后者的参数能将串流译码为 unarchive 的对象。

archive 的过程对应的是 encodeWithCoder:,而 unarchive 则是对应 initWithCoder:。表3列出 archive 与 unarchive 时,对象(变量)类型对应的 NSCoder 方法。

对象(变量)类型 对应的编码方法 对应的译码方法
Foundation Framework object - encodeObject:forKey: - decodeObjectForKey:
Boolean - encodeBool:forKey: - decodeBoolForKey:
int - encodeInt:forKey: - decodeIntForKey:
float - encodeFloat:forKey: - decodeFloatForKey:
double - encodeDouble:forKey: - decodeDoubleForKey:
char, byte 与 CString - encodeBytes:length:forKey: - decodeBoolForKey:

标准 C 语言中,基本变量类型的指针变量无法编码与译码,唯有字符数组能以 bytes 的方式进行,请读者特别注意。以下程序示范如何使用 NSCoding 协议的两个必要方法,制作一个支持 archive 的自定义类别。

范例程序 Subject.h
// archive_class_write/Subject.h
#import 

@interface Subject : NSObject  {
    NSString *_name;
    double _hours;
    int _score;
}

@property NSString *name;
@property double hours;
@property int score;

- (void)setSubject:(NSString*)tempName 
             hours:(double)tempHours score:(int)tempScore;
- (void)print;

@end
范例程序 Subject.m
// archive_class_write/Subject.m
#import "Subject.h"

@implementation Subject
- (void)setSubject:(NSString*)tempName 
             hours:(double)tempHours score:(int)tempScore
{
    _name = [tempName retain];
    _hours = tempHours;
    _score = tempScore;
}

- (void)print
{
    NSLog(@"\nSubject:%@, Hours:%g, Score:%i", self.name, self.hours, self.score);
}

// 基本类型与对象,编码方式
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    // [super encodeWithCoder:aCoder];
    [aCoder encodeObject:self.name forKey:@"SUBJECT_KEY"];
    [aCoder encodeDouble:self.hours forKey:@"HOURS_KEY"];
    [aCoder encodeInt:self.score forKey:@"SCORE_KEY"];
}

// 基本类型与对象,译码方式
- (id)initWithCoder:(NSCoder *)aDecoder
{
    // self = [super initWithCoder:aDecoder];
    self = [super init];
    if (self) {
    _name = [aDecoder decodeObjectForKey:@"SUBJECT_KEY"];
    _hours = [aDecoder decodeDoubleForKey:@"HOURS_KEY"];
    _score = [aDecoder decodeIntForKey:@"SCORE_KEY"];
    }
    return self;
}
@end

由上述程序了解,无论是何种类型对应的方法,都必须附上键值字符串,以上译码过程能以键值取得原来的内容。换言之,在使用 NSKeyedArchiver 时,会调用 encodeWithCoder: 方法,然后取得编码过的串流加以应用。相反的,在使用 NSKeyedUnarchiver 时,会调用 initWithCoder: 方法,将串流中的内容解码并存入对应的类别成员,最后再返回类别本身。若是根类已遵循 NSCoding 协议,并实现了这二个必要的方法,则 encodeWithCoder: 必须将下列语句插入至第一行:

[super encodeWithCoder:aCoder];

让根类别先行将它的类别成员编码至 aCoder,再接续由子类别进行此任务。同样的,initWithCoder:也必须将下列语句插入至第一行:

self = [super initWithCoder:aDecoder];

让根类别先将 aDecoder 内容中,自己的部分译码还原,再进行子类别的程序语句。这里也请注意,若是对象类型进行译码,必须自行 retain 对象,否则 aDecoder 回收时,也会将对象一并清除。以下为范例程序:

范例程序 archive_class_write
// archive_class_write
#import 
#import "Subject.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 储存路径
        NSString *path = [NSHomeDirectory()
                            stringByAppendingPathComponent:@"subject.plist"];
        // 创建数组
        NSMutableArray *SubArray = [[NSMutableArray alloc] init];
        Subject * sub1 = [[Subject alloc] init];
        Subject * sub2 = [[Subject alloc] init];
        Subject * sub3 = [[Subject alloc] init];
        [sub1 setSubject:@"C" hours:3.0 score:98];
        [sub2 setSubject:@"C++" hours:3.5 score:90];
        [sub3 setSubject:@"Objective-C" hours:3.5 score:92];
        [SubArray addObject:sub1];
        [SubArray addObject:sub2];
        [SubArray addObject:sub3];
        // 编码写入文件
        if (YES == [NSKeyedArchiver archiveRootObject:SubArray toFile:path]) 
        {
            NSLog(@"文件写入成功...");
        }
    }
    return 0;
}

程序首先建立三个 Subject 类型对象,并填入科目名称、上课时数与分数后,将它们依次存入 SubArray 数组。此时以 NSKeyedArchiver 的类别方法 archiveRootObject:toFile: 编码为 plist 格式后,存入 path 指定的文件。在编码 SubArray 的过程中,它的三个元素会先后进行编码,此是各类别的 encodeWithCoder: 方法依序被调用,依照方法设计的逻辑编码。下面的范例 archive_class_read 能将文件 subject.plist 复原为数组。

范例程序 archive_class_read
// archive_class_read
#import 
#import "Subject.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 文件储存路径
        NSString *path = [NSHomeDirectory()
                      stringByAppendingPathComponent:@"subject.plist"];
        NSMutableArray *SubArray;
        SubArray = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
        for (Subject *temp in SubArray) {
            [temp print];
        }
    }
    return 0;
}
输出结果
Subject:C, Hours:3, Score:98
Subject:C++, Hours:3.5, Score:90
Subject:Objective-C, Hours:3.5, Score:92
Program ended with exit code: 0

程序的重点是译码的过程中,触发各元素内的 initWithCoder: 方法,将文件的内容还原至 SubArray。

四、使用 NSData 定制 Archive 的程序

上述使用 NSKeyedArchiver(NSKeyedUnarchiver) 类别方法 archiveRootObject:toFile:(unarchiveObjectWithFile:),都是文件直接写入方式。若搭配文件输入与输出谈到的技巧,将多个需要的内容编码汇集后,再写入文件。这么做的好处能让程序更具弹性,并且在 I/O 的存取上更为快速。

范例程序 archive_nsdata_write
// archive_nsdata_write
#import 
#import "Subject.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 储存路径
        NSString *path = [NSHomeDirectory()
                       stringByAppendingPathComponent:@"data.plist"];
                       
        // 创建Subject对象
        Subject *sub1 = [[Subject alloc] init];
        Subject *sub2 = [[Subject alloc] init];
        [sub1 setSubject:@"Java" hours:4 score:80];
        [sub2 setSubject:@"Python" hours:3 score:82];
        
        // 创建NSData并与Archiver关联
        NSMutableData *data = [[NSMutableData alloc] init];
        NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] 
                                initForWritingWithMutableData:data];
        [archiver encodeObject:sub1 forKey:@"SUBJECT1_KEY"];
        [archiver encodeObject:sub2 forKey:@"SUBJECT2_KEY"];
        [archiver finishEncoding];
        
        // 写入文件中
        if (YES == [data writeToFile:path atomically:YES]) {
            NSLog(@"文件写入成功...");
        } else {
            NSLog(@"文件写入失败...");
        }
    }
    return 0;
}

首先建立并初始化两个 Subject 对象,接着建立 NSKeyedArchiver 类别对象 archiver,并且设定存入的数据暂存区域为 data,这里可以想象为一个串流。然后 archiver 使用 encodeObject:forKey: 方法,将 sub1 与 sub2 以对应的键值 SUBJECT1_KEY、SUBJECT1_KEY进行编码。请注意,这二行语句结束后,编码动作还没有执行,它仅是记载该被编码的对象是谁,直到下一行语句 [archiver finishEncoding]; 结束后,才会将对象编码并记录于 data 对象中,最后再利用 NSMutableData 的父类别方法 writeToFile:atomically: 将 data 对象写入文件。请注意,NSKeyedArchiver 默认是以二进制的 plist 储存,好处是文件较小,若想改以 XML plist 的格式储存,则可以加入下面这行语句:

[archiver setOutputFormat:NSPropertyListXMLFormat_v1_0];

请注意,NSKeyedUnarchiver 不需要设定输入格式,它会自动判断。这里除了这两种格式外,Mac OS X 另外支持自 NextStep 而来的 NSPropertyListOpenStepFormat,也称之为旧格式 ASCII plist,但因为是旧格式,因此只可读取不可写入。plist 可用的三种格式如下表所示:

plist 格式 说明
NSPropertyListOpenStepFormat old-style ASCII plist,旧格式,只可进行读取
NSPropertyListXMLFormat XML plist,能以纯文本编辑器直接编辑
NSPropertyListBinaryFormat Binary plist,同样内容的情况下,比较XML plist文件还小

范例程序 archive_nsdata_read 能将上一个范例输出的文件 data.plist 解码还原。

范例程序 archive_nsdata_read
// archive_nsdata_read
#import 
#import "Subject.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 储存路径
        NSString *path = [NSHomeDirectory()
                     stringByAppendingPathComponent:@"data.plist"];
                     
        // 创建Subject对象
        Subject *sub1;
        Subject *sub2;
        
        // 将储存路径 path 传给 data 读取,并设定 unarchiver 初始参数
        NSMutableData *data = [NSMutableData dataWithContentsOfFile:path];
        NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] 
                                    initForReadingWithData:data];
                                    
        // 以对应的键值进行译码
        sub1 = [unarchiver decodeObjectForKey:@"SUBJECT1_KEY"];
        sub2 = [unarchiver decodeObjectForKey:@"SUBJECT2_KEY"];
        
        // 关闭 unarchiver,结束解码动作
        [unarchiver finishDecoding];
        
        // 显示 sub1 与 sub2 的内容
        [sub1 print];
        [sub2 print];
    }
    return 0;
}

较为重要的地方在于,译码与编码过程完全是相反的,首先利用 NSMutableData 的打开文件方法,将 data.plist 文件读入 data 串流,接着建立并初始化 NSKeyedUnarchiver,这里是将 data 串流设定为 unarchiver 欲解码的串流。前置作业结束后,主要的解码便是将指定的键值所对应的对象,返回给欲参考的变量 sub1 与 sub2,最后别忘记了将 finishDecoding 信息传送给 unarchiver,以完成译码的相关动作。

五、使用 Archiver 完成深层复制

由于 archive 保留对象图形的结构与内容,因此能利用这个特性达到深层复制。下面的范例程序 archvie_nsdata_copy 示范如何进行此项功能。

范例程序 archvie_nsdata_copy
// archvie_nsdata_copy
#import 
#import "Subject.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSData *data;
        NSMutableArray *array1 = [NSMutableArray arrayWithObjects:
                                 [NSMutableString stringWithString:@"no.1"],
                                 [NSMutableString stringWithString:@"no.2"],
                                 [NSMutableString stringWithString:@"no.3"],
                                 nil];
        NSMutableArray *array2;
        NSMutableString *str;
    
        // 显示出数组参考的地址
        NSLog(@"array1对象的地址:%lx",array1);
        NSLog(@"array1的元素地址:");
        for (int index = 0; index < [array1 count]; index ++) {
            NSLog(@"index %i: %lx", index, [array1 objectAtIndex:index]);
        }
    
        // 显示出两数组修改前的内容
        NSLog(@"======修改前======");
        NSLog(@"array1的元素:");
        for (NSString *temp in array1) {
            NSLog(@"%@",temp);
        }
        NSLog(@"array2的元素:");
        for (NSString *temp in array2) {
            NSLog(@"%@",temp);
        }
    
        // 以 data 当作中介,将 array1 放进 data 后,再给 array2
        data = [NSKeyedArchiver archivedDataWithRootObject:array1];
        array2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    
        // 经由 NSKeyedArchiverr 的运行后...
        NSLog(@"经由 NSKeyedArchiverr 的运行后...");
        NSLog(@"array2对象的地址:%lx",array2);
        NSLog(@"array2的元素地址:");
        for (int index = 0; index < [array2 count]; index ++) {
            NSLog(@"index %i: %lx", index, [array2 objectAtIndex:index]);
        }
    
        // 修改 array2 位于 index 为 0 的位置,在最后插入字符串
        str = [array2 objectAtIndex:0];
        [str appendString:@"additional"];
    
        // 显示出两数组的内容,以验证复制结果
        NSLog(@"======修改后======");
        NSLog(@"array1的元素:");
        for (NSString *temp in array1) {
            NSLog(@"%@",temp);
        }
        NSLog(@"array2的元素:");
        for (NSString *temp in array2) {
            NSLog(@"%@",temp);
        }
    }
    return 0;
}

程序首先建立具有三个 NSMutableString 元素的 NSMutableArray,接着以语句:

data = [NSKeyedArchiver archivedDataWithRootObject:array1];

将根对象 array1 进行编码,返回的内容为 NSMutableData 串流对象并指定给 data,接着语句:

array2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];

将串流对象 data 当作参数,让 NSKeyedUnarchiver 将此串流的内容进行译码,因此 array2 得到一个 id 类型的对象,由此可知,此方法只适合 objective-C 的对象,因此标准 C 语言的对象,得先转换成 NSNumber 或 NSString 等对象,才适用此方法。为了验证此方法,这里先将原数组内容显示出来后,再改变 array2 位于索引值为 0 的对象内容,接着再显示出这两个数组,输出结果如下:

输出结果
array1对象的地址:1002006d0
array1的元素地址:
index 0: 100200400
index 1: 100200490
index 2: 1002004d0
======修改前======
array1的元素:
no.1
no.2
no.3
array2的元素:
经由 NSKeyedArchiverr 的运行后...
array2对象的地址:100603150
array2的元素地址:
index 0: 100602080
index 1: 100602370
index 2: 1006023b0
======修改后======
array1的元素:
no.1
no.2
no.3
array2的元素:
no.1additional
no.2
no.3

此方法可以解决相当多隐藏的复制问题,若欲建立的应用程序,无法以 NSCoding 协议所提出的解决方案处理,那么使用此方法将这将它实现 Copy 或 MutableCopy 方法中,以达深层复制的功能。

小结

本章介绍了 Foundation 框架的归档和序列化类。使用 Foundation 框架归档和序列化类可以将对象(对象图)转换为具有独立结构的字节缓冲区。这样就可以将数据写入文件或者传送给其他进程(通常会通过网络)。之后,这些数据可能会被转换回对象并保留相关的对象图。序列化处理类能够保存数据和对象在其层次结构中的位置,而归档处理类具有更广泛的用途,它们可以保存数据、数据类型和对象层次结构中对象之间的关系。

你可能感兴趣的:(Objective-C 学习笔记 - 第13章 归档和序列化)