NSCoding / NSKeyed​Archiver

在构建应用程序时,一个重要的架构决策问题是在每次启动之间如何持久化数据。问题是如何精准的重现最后一次关闭应用前的状态;如何描述对象图以使下次完美地重新构建。

在 iOS 和 OS X 上, 苹果提供了两种选择 :Core DataNSKeyedArchiver / NSKeyedUnarchiver (用来将遵循 <NSCoding> 的类序列化)

或者更确切地说:有三种选择,如果你算上NSURLCache的话. 在client-server应用场景下,在每次启动时加载必要的数据是一种可行的设计,尤其是结合磁盘缓存,存储服务器的响应,这样当发送对应请求的时候可以立即返回。在实践中,网络层和对象层上的缓存结合是可取的。

当涉及到建模,查询,遍历,持久化复杂的对象图,那Core Data是无可代替的。Core Data 是把大锤子,但不是所有的问题都是足够大的钉子。

Core Data 和 NSKeyedArchiver客观和常见的比较可能是这样的:

Core Data NSKeyedArchiver
Entity Modeling Yes No
Querying Yes No
Speed Fast Slow
Serialization Format SQLite, XML, or NSData NSData
Migrations Automatic Manual
Undo Manager Automatic Manual

等等。在这场对决中,没有什么可比性,它看起来是一边倒的局势。

…也就是说,直到你从一个稍微不同的角度看它:

Core Data NSKeyedArchiver
Persists State Yes Yes
Pain in the Ass Yes No

通过这些对比,在某些情况下NSKeyedArchiver是一个完全合理的选择。并非所有的应用程序需要查询的数据。并非所有的应用程序需要自动迁移。并非所有的应用程序处理大型或复杂的对象图。而且应用中确实是有一些模块更好地被一个简单的方案解决。

这篇文章将关注如何,何时,为什么选择NSKeyedArchiverNSCoding。希望能提供给亲爱的读者们以启发选择最合适的工具。


NSCoding 是一个简单的协议,有两个方法: -initWithCoder:encodeWithCoder:。遵循NSCoding协议的类可以被序列化和反序列化,这样可以归档到磁盘上或分发到网络上。

举个例子:

@interface Book : NSObject <NSCoding>
@property NSString *title;
@property NSString *author;
@property NSUInteger pageCount;
@property NSSet *categories;
@property (getter = isAvailable) BOOL available;
@end

@implementation Book

#pragma mark - NSCoding

- (id)initWithCoder:(NSCoder *)decoder {
    self = [super init];
    if (!self) {
        return nil;
    }

    self.title = [decoder decodeObjectForKey:@"title"];
    self.author = [decoder decodeObjectForKey:@"author"];
    self.pageCount = [decoder decodeIntegerForKey:@"pageCount"];
    self.categories = [decoder decodeObjectForKey:@"categories"];
    self.available = [decoder decodeBoolForKey:@"available"];

    return self;
}

- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:self.title forKey:@"title"];
    [encoder encodeObject:self.author forKey:@"author"];
    [encoder encodeInteger:self.pageCount forKey:@"pageCount"];
    [encoder encodeObject:self.categories forKey:@"categories"];
    [encoder encodeBool:[self isAvailable] forKey:@"available"];
}

@end

如上,NSCoding 主要是样板文件。每个属性用属性名作为key,编码或解码成一个对象或者类型。(有些开发者喜欢定义NSString *常量用作keypath, 其实通常这是没必要的)

但是在控制整个序列化过程中样板文件有时候也是非常有用的东西,它可以保持灵活,可以这样解释:

  • Migrations: 如果一个数据模型发生变化,如添加,重命名或删除一个字段,这应该与之前序列化的数据保持兼容性。Apple提供了一些参考“Forward and Backward Compatibility for Keyed Archives”.

  • Archiving non-NSCoding-compatible Classes: 根据面向对象设计原则,对象应该可以被编码和解码成一种序列化的格式。但是如果一个类不是内置遵循NSCoding,你可以后续让这个类遵循NSCoding来达到目的。

Mantle是一个意在减少写NSCoding样板文件的类库。如果你正在寻找更方便使用NSCoding代替Core Data创建model的方法,那Mantle值得一看。


当然,序列化只是故事的一部分。决定把数据持久化到什么地方又是另一个问题。同样,这里有两个方法:写入本地文件系统或者使用NSUserDefaults

File System

NSKeyedArchiverNSKeyedUnarchiver 提供了很方便的API把对象读取/写入磁盘。

一个基于NSCoding的table view controller可以通过file manager设置它的属性集合。

Archiving

[NSKeyedArchiver archiveRootObject:books toFile:@"/path/to/archive"];

Unarchiving

[NSKeyedUnarchiver unarchiveObjectWithFile:@"/path/to/archive"];

NSUserDefaults

每个应用程序都有自己的user preferences,它可以存储和检索遵循NSCoding协议的对象或者是C类型数据。

然而不推荐将整个对象图存入NSUserDefaults,但是用这种方式编码复合对象是不错的选择,例如“当前用户”的对象或者API credentials(用 Keychain 来代替)

Archiving

NSData *data = [NSKeyedArchiver archivedDataWithRootObject:books];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"books"];

Unarchiving

NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:@"books"];
NSArray *books = [NSKeyedUnarchiver unarchiveObjectWithData:data];

作为开发者,我们是有义务去理解我们应用程序的目标和需求,并且抑制住过于工程化和过早优化的冲动。

在应用程序中使用Core Data的决定或许是显而易见的。但是在很多情况下,Core Data 被发现是如此的笨拙或者不必要的,甚至是一种对可用性的障碍,更不用说实用性。

即使很多应用在某些方面_可能_受益于Core Data,让事物从简单演化到复杂是必要的,其中蕴含着智慧。但是就持久化而言,它不会比NSCoding更简单。

作者 Mattt
Mattt

Mattt (@mattt) is a writer and developer in Portland, Oregon. He is the founder of NSHipster and Flight School, and the creator of several open source libraries, including AFNetworking and Alamofire.

翻译者
下一篇文章

上周,我们感觉Core Data有些难,所以为了NSHipster的这个问题,我们将奉上关于使用Core Data的最好开源库的引导。仔细阅读,看看你如何充分利用这次Core Data体验。