在构建应用程序时,有一个重要的问题是如何在每次启动之间持久化数据,以便重现最后一次关闭应用前的状态。在iOS和OS X上,苹果提供了三种选择:Core Data、属性列表(Property List)和带键值的编码(NSKeyedArchiver)。当涉及到建模、查询、遍历、持久化等复杂的对象图时,Core Data无可替代。但并非所有应用程序都需要查询数据、处理复杂对象图,有时候使用NSKeyedArchiver更为简单。

1. 使用NSKeyedArchiver

如果要将各种类型的对象存储到文件中,而不仅仅是字符串、数组、字典类型,利用NSKeyedArchiver类创建带健(keyed)的档案来完成将非常灵活。

在带健的档案中,会为每个归档对象提供一个名称,即健(key)。根据这个key可以从归档中检索该对象。这样,就可以按照任意顺序将对象写入归档并进行检索。另外,如果向类中添加了新的实例变量或删除了实例变量,程序也可以进行处理。

NSKeyedArchiver存储在硬盘上的数据是二进制格式:

 
KeyedArchiverBinaryData.png

你可以通过文本编辑器打开二进制文件,但一般来说没有必要。二进制文件是为计算机而设计,比纯文本文件占用磁盘空间小,并且加载速度也更快。例如,Interface Builder通常以二进制格式存储NIB文件。

下面我们结合代码来学习归档与解档:

创建Single View Application模板的demo,demo名称为KeyedArchiver。在storyboard中添加四个UILabel、四个UITextField和两个UIButton。布局如下:

 
KeyedArchiverStoryboard.png

当点击Archive按钮时,把NameAge对应的文本框内容归档到/Library/Application Support内的文件夹。当点击Unarchiver按钮时,把刚创建归档程序读入执行程序中,并对应的显示到下面的两个文本框中。

拖拽文本框IBOutlet属性到ViewController.m接口部分,拖转两个UIButton的IBAction到实现部分,分别命名为archiver:unarchiver:。完成后如下:

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITextField *nameArchiver;
@property (weak, nonatomic) IBOutlet UITextField *ageArchiver;
@property (weak, nonatomic) IBOutlet UITextField *nameUnarchiver;
@property (weak, nonatomic) IBOutlet UITextField *ageUnarchiver; @end
- (IBAction)archiver:(UIButton *)sender {

}

- (IBAction)unarchiver:(UIButton *)sender {

}

在声明部分添加一个NSString类型的documentsPath对象,并使用懒加载初始化。该对象为沙盒中Documents\Application Support\目录。这样只需要获取一次路径就可以重复使用,有助于提高性能。

@interface ViewController ()

...
@property (strong, nonatomic) NSString *documentsPath; @end
- (NSString *)documentsPath {
if (!_documentsPath) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
if (paths.count > 0) {
_documentsPath = paths.firstObject; // 如果目录不存在,则创建该目录。
if (! [[NSFileManager defaultManager] fileExistsAtPath:_documentsPath]) {
NSError *error;
// 创建该目录
if(! [[NSFileManager defaultManager] createDirectoryAtPath:_documentsPath withIntermediateDirectories:YES attributes:nil error:&error])
{
NSLog(@"Failed to create directory. error: %@",error);
}
}
}
}
return _documentsPath;
}

在初始化documentsPath时,使用NSSearchPathForDirectoriesInDomain()方法获取Library/Application Support/目录,如果目录不存在,则创建该目录。

对于NSStringNSArrayNSDictionaryNSSetNSDateNSNumberNSData之类的基本Objective-C类对象,都可以直接使用NSKeyedArchiver归档和NSKeyedUnarchiver读取归档文件。

更新archiver:方法,当点击Archiver按钮时对nameArchiverageArchiver中的文本进行归档。

- (IBAction)archiver:(UIButton *)sender {
// A 使用archiveRootObject: toFile: 方法归档
// 1.修改当前目录为self.documentsPath
NSFileManager *sharedFM = [NSFileManager defaultManager];
[sharedFM changeCurrentDirectoryPath:self.documentsPath]; // 2.归档
if (![NSKeyedArchiver archiveRootObject:self.nameArchiver.text toFile:@"nameArchiver"]) {
NSLog(@"Failed to archive nameArchiver");
}
if (![NSKeyedArchiver archiveRootObject:self.ageArchiver.text toFile:@"ageArchiver"]) {
NSLog(@"Failed to archive ageArchiver");
}
}

上述代码分步说明如下:

  1. 使用NSFileManager修改当前工作目录为self.documentsPath
  2. 使用archiveRootObject: toFile:方法将文本框中的文本进行归档,该方法返回值为BOOL类型,归档成功返回YES,归档失败返回NO。这里的toFile:参数@"nameArchiver"@"ageArchiver"均为相对路径,相对于1中设定的当前路径。

这篇文章会多次用到文件系统和NSFileManager,如果你还不熟悉,可以查看我的另一篇文章:使用NSFileManager管理文件系统

再更新unarchiver:方法,当点击Unarchiver按钮时读取归档文件,并对应地显示到nameUnarchiverageUnarchiver中。

- (IBAction)unarchiver:(UIButton *)sender {
// A 使用unarchiveObjectWithFile: 读取归档
// 1.获取归档路径
NSString *nameArchiver = [self.documentsPath stringByAppendingPathComponent:@"nameArchiver"];
NSString *ageArchiver = [self.documentsPath stringByAppendingPathComponent:@"ageArchiver"]; // 2.读取归档,并将其显示在对应文本框。
self.nameUnarchiver.text = [NSKeyedUnarchiver unarchiveObjectWithFile:nameArchiver];
self.ageUnarchiver.text = [NSKeyedUnarchiver unarchiveObjectWithFile:ageArchiver];
}

上述代码的分步说明如下:

  1. 使用stringByAppendingPathComponent:方法获取归档路径,这里也可以使用归档方法中设置当前路径的方法,两种方法效果一样。
  2. 使用NSKeyedUnarchiver类的unarchiverObjectWithFile:方法从路径中读取归档,并赋值给对应文本框。

运行demo,在上面两个UITextField中输入文本,点击Archiver按钮即可把文本框中的文本归档。点击Unarchiver按钮即可读取归档数据,并将其显示到对应文本框。

 
KeyedArchiverA.gif

2. 编码方法和解码方法

前面我们说过,对于NSStringNSArray等基本的Objective-C类对象,都可以直接使用NSKeyedArchiverNSKeyedUnarchiver进行归档和解档。而对于其他类型的对象,则必须告知系统如何编码你的对象,以及如何解码。这时你的类必须遵守NSCoding协议,该协议只有两个必须实现的方法encodeWithCoder:initWithCoder:

为遵守面向对象的设计原则,被编码、解码的对象负责对其实例变量进行编码和解码。编码器通过调用encodeWithCoder:initWithCoder:方法指导对象编码、解码其实例。encodeWithCoder:指导对象编码其实例变量至该方法参数中的编码器,该方法可能会被调用多次;initWithCoder:指导对象用参数中的数据初始化自身,它会替换任何其他初始化方法,并且每个对象仅发送一次。必须遵守NSCoding协议、实现这两个方法,该类才可以对其实例进行编码、解码。

继续上面的demo,添加一个模版为Cocoa Touch Class,类名为Person,父类为NSObject的文件。

Person.h中添加以下属性和方法:

@interface Person : NSObject

@property (strong, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger age; - (void)setName:(NSString *)name age:(NSInteger)age; @end

Person.m实现setName: age:方法。

- (void)setName:(NSString *)name age:(NSInteger)age {
self.name = name;
self.age = age;
}

下面是解码方法和编码方法。

// 1.编码方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"PersonName"];
[aCoder encodeInteger:self.age forKey:@"PersonAge"];
} // 2.解码方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
self.name = [aDecoder decodeObjectForKey:@"PersonName"];
self.age = [aDecoder decodeIntegerForKey:@"PersonAge"];
}
return self;
}

上述代码的分步说明如下:

  1. 该程序向编码方法encodeWithCoder:传入一个NSCoder对象作为参数。由于Person类直接继承自NSObject,所以无需担心编码继承的实例变量。如果的确担心,并且知道类的父类符合NSCoding协议,那么在编码方法开始处添加[super encodeWithCoder: encoder],确保继承的实例变量也被编码。另外,不同类型对象使用不同编码方法。如果编码NSString类型对象,使用encodeObject: forKey:方法,如果编码NSInteger类型对象,使用encodeInteger: forKey:方法。这里的键名是任意的,只要跟解码时的一致即可。为防止子类和父类使用相同键而导致冲突,可以像这里定义的一样,制定键名时将类名放在键名前加以区分。
  2. 解码过程与编码刚好相反。传递给initWithCoder:的参数也是NSCoder对象,不用担心这个参数,只要记住它是想要从归档中提取的对象即可。如果担心解码继承的实例变量,且该父类遵守NSCoding协议,可以用self = [super initWithCoder: decoder];开始解码方法。只要键与编码时相同就可以解码。

进入ViewController.m方法,导入Person.h,并在接头部分添加以下属性:

@interface ViewController ()

...
@property (strong, nonatomic) Person *person; @end

最后记得在viewDidLoad方法中初始化该方法。

注释掉archiver:方法内代码,并添加以下代码,以便归档Person类。

- (IBAction)archiver:(UIButton *)sender {
// B 使用initForWritingWithData: 归档。
// 1.把当前文本框内文本传送给person。
[self.person setName:self.nameArchiver.text age:[self.ageArchiver.text integerValue]]; // 2.使用initForWritingWithMutableData: 方法归档内容至mutableData。
NSMutableData *mutableData = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:mutableData];
[archiver encodeObject:self.person forKey:@"person"];
[archiver finishEncoding]; // 3.把归档写入Library/Application Support/Data目录。
NSString *filePath = [self.documentsPath stringByAppendingPathComponent:@"Data"];
if (![mutableData writeToFile:filePath atomically:YES]) {
NSLog(@"Failed to write file to filePath");
}
}

上述代码的分布说明如下:

  1. 把当前文本框内文本传送给person。记得在`viewDidLoad
  2. 先创建一个空缓冲区,其大小将随着程序执行的需要而扩展。通过intiForWritingWithMutableData:方法以指定归档数据的存储空间为mutableData,现在可以向archiver对象发送编码消息,以便归档对象,这里可以归档多个对象。所有对象都归档后必须向archiver发送finishEncoding消息。在此之后,就不能编码其他对象了。此时,你预留的mutableData区域包含归档对象。
  3. 使用writeToFile: atomically:方法把归档后的对象写入文件,该方法返回值为BOOL类型,写入成功时返回YES;操作失败时返回NOatomically:参数为YES表示希望首先将文件写入到临时备份中,且一旦成功,将把该备份重命名为指定目录名。这是一种安全措施,可以避免文件在操作过程中因系统崩溃而致使原文件、新文件均损坏。如果参数为NO,则会直接在指定目录写入文件。

同样,注释掉unarchiver:中原来代码,并添加以下代码读取归档。

- (IBAction)unarchiver:(UIButton *)sender {
...
// B 使用initForReadingWithData: 读取归档。
// 1.从Library/Application Support/Data目录获取归档文件。
NSString *filePath = [self.documentsPath stringByAppendingPathComponent:@"Data"];
NSData *data = [NSData dataWithContentsOfFile:filePath]; // 2.使用initForReadingWithData: 读取归档。
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
self.person = [unarchiver decodeObjectForKey:@"person"];
[unarchiver finishDecoding]; // 3.把读取到的内容显示到对应文本框。
self.nameUnarchiver.text = self.person.name;
self.ageUnarchiver.text = [@(self.person.age) stringValue];
}

读取归档时,首先通过dataWithContentsOfFile:方法获取归档文件,之后使用initForReadingWithData:读取归档,在解码结束时,一定要向unarchiver发送finishDecoding消息结束解码。最后将读取到的归档内容显示到对应文本框。

运行app,可以像之前一样对文本框内容进行归档、解档。

如果想要了解属性列表及通过代码练习,可以查看这篇文章:使用偏好设置、属性列表、归档解档保存数据、恢复数据

你也可以尝试注释掉Person.m中的编码方法和解码方法再次运行demo,点击按钮时会在控制台看到出错消息。

你也可以通过添加观察者,在应用程序进入后台时(通知为UIApplicationDidEnterBackgroundNotifiaction)归档文件,这样即使app被终止,数据也不会丢失。你可以自行完成,如果遇到问题,可以通过文章底部网址获取源码查看。

3. 使用归档程序复制对象

可以使用归档功能实现深复制,可以将对象归档到一个缓冲区,然后把它从缓冲区解归档,这样就实现了深复制。如下所示:

    NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"one"], [NSMutableString stringWithString:@"two"], nil];

    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:mutableArray];
NSMutableArray *mutableArray2 = [NSKeyedUnarchiver unarchiveObjectWithData:data];

如果你对其是否进行了深复制有疑惑,可以通过修改其中一个数组的元素,查看另一个数组内元素是否改变来验证。

深入了解深复制、浅复制,查看深复制、浅复制、copy、mutableCopy这篇文章。

4. 三种归档方法的区别

在这篇文章中使用了archiveRootObject: toFile:initForWritingWithMutableData:archivedDataWithRootObject:三种类型归档方法,它们区别如下:

  1. archiveRootObject: toFile:不能决定如何处理归档的数据,直接被写入了文件。
  2. initForWritingWithMutableData:归档的数据可以通过网络分发,除此之外还可以把多个对象归档到一个缓冲区。
  3. archivedDataWithRootObject:这种方法归档的数据可以通过网络分发,非常灵活。

总之,只是方便与灵活的区别。

Demo名称:KeyedArchiver
源码地址:https://github.com/pro648/BasicDemos-iOS

参考资料:

  1. NSCoding / NSKeyed​Archiver
  2. Differences with archiveRootObject:toFile: and writeToFile:
  3. Saving Application Data

作者:pro648
来源:简书

数据存储之归档解档 NSKeyedArchiver NSKeyedUnarchiver的更多相关文章

  1. IOS数据存储之归档/解档

    前言: 前天学习了NSUserDefaults,我们知道NSUserDefaults不能保存自定义对象,所以我们今天来认识一下归档(NSKeyedArchiver)和解档(NSKeyedUnarchi ...

  2. IOS s数据存储之归档解档

    #import <Foundation/Foundation.h> @interface Student : NSObject <NSCoding>; @property(no ...

  3. 数据持久化------Archiving(归档,解档)

    其中TRPerson为自定义的继承自NSObject的类的子类  其中有两个属性,name 和 age .h文件 #import @interface TRPerson : NSObject<& ...

  4. iOS开发——UI进阶篇(十一)应用沙盒,归档,解档,偏好设置,plist存储,NSData,自定义对象归档解档

    1.iOS应用数据存储的常用方式XML属性列表(plist)归档Preference(偏好设置)NSKeyedArchiver归档(NSCoding)SQLite3 Core Data 2.应用沙盒每 ...

  5. iOS开发中的4种数据持久化方式【一、属性列表与归档解档】

    iOS中的永久存储,也就是在关机重新启动设备,或者关闭应用时,不会丢失数据.在实际开发应用时,往往需要持久存储数据的,这样用户才能在对应用进行操作后,再次启动能看到自己更改的结果与痕迹.ios开发中, ...

  6. iOS开发UI篇—ios应用数据存储方式(归档)

    iOS开发UI篇—ios应用数据存储方式(归档)  一.简单说明 在使用plist进行数据存储和读取,只适用于系统自带的一些常用类型才能用,且必须先获取路径相对麻烦: 偏好设置(将所有的东西都保存在同 ...

  7. iOS开发UI篇—ios应用数据存储方式(归档) :转发

    本文转发至:文顶顶http://www.cnblogs.com/wendingding/p/3775293.html iOS开发UI篇—ios应用数据存储方式(归档)  一.简单说明 在使用plist ...

  8. iOS 浅复制、深复制、完全复制的知识点梳理验证(附加归档解档)

    在之前转载的一片文章中,文中对浅复制和深复制进行了详细的解读,同时还提到了深复制(one-level-deep copy).完全复制(true copy)的概念,并指出iOS开发中的深复制是单层深赋值 ...

  9. 归档 & 解档

    代码实现 遵守协议 class AccessToken: NSObject, NSCoding 实现协议方法 // MARK: - 归档&解档 required init(coder aDec ...

随机推荐

  1. OkHttp3源码详解(三) 拦截器-RetryAndFollowUpInterceptor

    最大恢复追逐次数: ; 处理的业务: 实例化StreamAllocation,初始化一个Socket连接对象,获取到输入/输出流()基于Okio 开启循环,执行下一个调用链(拦截器),等待返回结果(R ...

  2. ISO14971-2007阅读

    1.什么是风险? 风险的概念,公认的组成有两部分: 损害发生的概率 损害的后果,即损坏的严重性 2.风险管理的适用范围? 适用于医疗器械生命周期所有阶段 不适用于临床判断 不要求具体的质量体系,但14 ...

  3. 平均负载(Load average)

    load average 的含义平均负载(load average)是指系统的运行队列的平均利用率,也可以认为是可运行进程的平均数. top命令中load average显示的是最近1分钟.5分钟和1 ...

  4. ExpressRoute 常见问题

    什么是 ExpressRoute? ExpressRoute 是一项 Azure 服务,允许在 Microsoft 数据中心与本地环境或共同租用设施中的基础结构之间创建专用连接. ExpressRou ...

  5. Sqlserver新建随机测试数据

    USE Test --使用数据库Test(如果没有则需要新建一个) ----1.新建一个users表 create table users( uId int primary key identity( ...

  6. Windows as a Service(1)—— Windows 10服务分支

    前言 作为公司的IT管理员,管理全公司Windows 10操作系统的更新一直是工作中的头疼之处.微软提供了很多方法来帮助我们管理公司的Windows 10更新,比如Windows Server Upd ...

  7. 最优化作业 共轭梯度法 matlab代码

    syms f x1 x2 f=(1/2)*x1^2+x2^2; x=[2;1]; a=[1 0;0 2];% A g1=diff(f,x1); g2=diff(f,x2); g=[g1;g2];%导数 ...

  8. Android SDK 墙内更新方法

    1.访问地址:http://ping.chinaz.com/,在网站测速处输入g.cn执行查看分析,如下图所示 2.在分析列表中找到速度最快的IP复制进行设置SDK代理并设置端口为80,并勾选Forc ...

  9. 008单例、继承、final

    内容:单例,类继承,final #################################################################################### ...

  10. BZOJ 1051 受欢迎的牛 缩点

    题目链接: https://www.lydsy.com/JudgeOnline/problem.php?id=1051 题目大意: 每一头牛的愿望就是变成一头最受欢迎的牛.现在有N头牛,给你M对整数( ...