A Day In The Life

とあるプログラマの備忘録

iOS でオブジェクトをシリアライズしてファイルに保存する方法

iOS でデータを永続化する方法の続きです。今回はシリアライズされたオブジェクトの保存方法について説明します。シリアライズされたオブジェクトはファイルで保存することが容易なためデータ永続化の際に頻繁に使用されます。
シリアライズ自体はデータの保存に限らず、Interface Builder やネットワークを使ったデータの送受信などいろいろなところで使われています。
プログラマであれば必ずおさえておきたい技術の一つです。

シリアライズって何?

オブジェクトの状態をバイナリ(0と1の集まり)に変換することをオブジェクトのシリアライズまたはシリアル化といいます。逆にバイナリをオブジェクトに変換することをデシリアライズといいます。
シリアライズされたデータは iOS 上では NSData オブジェクトとしてあつかわれます。NSData オブジェクトはそのままファイルに保存することができます。
iOS ではシリアライズすることをアーカイブ、デシリアライズすることをアンアーカイブといいます。それぞれアーカイブのための NSKeyedArchiver クラスとアンアーカイブのための NSKeyedUnarchiver クラスが提供されています。
図にすると以下のようになります。
アーカイブ
オブジェクトのシリアライズという行為はオブジェクトをバイナリに変換することまでを差します。バイナリをファイルに保存することは含みません。「オブジェクトのシリアライズ=永続化」ではないので注意が必要です。

NSKeyedArchiver を使ってデータをファイルに保存する

それでは具体的にアーカイブする方法を見ていきましょう。
NSKeyedArchiver クラスの arrayWithObjects: メソッドを使うとオブジェクトをアーカイブすることができます。先ほども説明しましたがアーカイブされたデータは NSData オブジェクトに変換されます。
以下はその例です。

NSArray *array = NSArray *array = @[@"山田太郎", @"東京都中央区"];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array];

アーカイブされたデータは NSData クラスの arrayWithObjects:atomically: メソッドでファイルに保存することもできますが、NSKeyedArchiver クラスの archiveRootObject:toFile: メソッドを使うとオブジェクトのアーカイブからファイル保存まで一括で行ってくれます。
以下は NSArray オブジェクトをアーカイブしてファイルに保存する例です。

NSString *directory = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
NSString *filePath = [directory stringByAppendingPathComponent:@"data.dat"];

NSArray *array = @[@"山田太郎", @"東京都中央区"];
BOOL successful = [NSKeyedArchiver archiveRootObject:array toFile:filePath];
if (successful) {
  NSLog(@"%@", @"データの保存に成功しました。");
}

アーカイブされたデータを読み込む

アーカイブされたデータを元に戻すには NSKeyedUnarchiver クラスの unarchiveObjectWithData: メソッドを使います。
以下はその例です。

NSArray *before = @[@"山田太郎", @"東京都中央区"];
// オブジェクトのアーカイブ
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:before];
// オブジェクトのアンアーカイブ
NSArray *after = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if ([before isEqualToArray:after]) {
  NSLog(@"%@", @"同じオブジェクトです。");
}

実行結果は以下のようになります。

2011-09-04 21:51:57.073 DataManagement[10053:707] 同じオブジェクトです。

またファイルに保存されたバイナリデータを復元するには NSKeyedUnarchiver クラスの unarchiveObjectWithFile: メソッドを使います。
以下はその例です。

NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
if (array) {
  for (NSString *data in array) {
    NSLog(@"%@", data);
  }
} else {
  NSLog(@"%@", @"データが存在しません。");
}

アーカイブできるオブジェクトの種類

オブジェクトにはアーカイブできるものと出来ないものがあります。
NSKeyedArchiver でアーカイブできるクラスのオブジェクトは以下の通りです(サブクラスのオブジェクトもアーカイブ可能です)。

  • NSArray
  • NSDictionary
  • NSString
  • NSDate
  • NSNumber
  • NSData
  • NSURL
  • UIView
  • UIViewController
  • その他 NSCoding プロトコルに準拠しているクラス

UIView と UIViewController は Interface Builder でインスタンス化された時にこの仕組みが使われています。これら両クラスのオブジェクトをプログラマが意識してアーカイブすることはあまりないと思います。

自作クラスのオブジェクトをアーカイブしてファイルに保存する

オブジェクトにはアーカイブできるものとできないものがあると書きましたが、自作クラスのオブジェクトでも NSCoding プロトコルに準拠していればアーカイブする事ができます。
ここでは関連を持った Person クラスと Address クラスのオブジェクトをアーカイブするプログラムを例に説明します。
PersonクラスとAddressクラス
まず NSCoding プロトコルについて説明していきます。
NSCoding プロトコルには以下のように encodeWithCoder: メソッドと initWithCoder: メソッドが定義されています。

@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (id)initWithCoder:(NSCoder *)aDecoder;
@end

それぞれメソッドの役割は以下のようになっています。

  • encodeWithCoder:
    オブジェクトをアーカイブするときに呼ばれるメソッド
  • initWithCoder:
    アーカイブされたファイルからオブジェクトの状態を復元するときに呼ばれるメソッド

クラスを NSCoding プロトコルに準拠させるためには NSCoding プロトコルの宣言とこれらメソッドを実装する必要があります。
Person クラスと Address クラスの例でこれらを図にすると以下のようになります。
NSCodingの実装
それでは実際のプログラムを見ていきましょう。
はじめに Person クラスと Address クラスの定義に NSCoding プロトコルの宣言を追加します。

@interface Person : NSObject <NSCoding> {
}
@end
@interface Address : NSObject <NSCoding> {
}
@end

次に encodeWithCoder: と initWithCoder: メソッドを追加します。
まずは Person クラスの実装から

@interface Person : NSObject <NSCoding>

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) Address *address;

@end

@implementation Person
- (id)initWithCoder:(NSCoder *)decoder
{
  self = [super init];
  if (self) {
    _name = [decoder decodeObjectForKey:@"name"];
    _address = [decoder decodeObjectForKey:@"address"];
  }
  return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder
{
  [encoder encodeObject:name forKey:@"name"];
  [encoder encodeObject:address forKey:@"address"];
}
- (void)dealloc
{
    self.name = nil;
    self.address = nil;
}
@end

次に Address クラスの実装です。

@interface Address : NSObject <NSCoding>

@property (nonatomic, strong) NSString *zipCode;
@property (nonatomic, strong) NSString *state;
@property (nonatomic, strong) NSString *city;
@property (nonatomic, strong) NSString *other;

@end

@implementation Address
@synthesize zipCode = _zipCode, state = _state, city = _city, other = _other;
- (id)initWithCoder:(NSCoder *)decoder
{
  self = [super init];
  if (self) {
    _zipCode = [decoder decodeObjectForKey:@"zipCode"];
    _state = [decoder decodeObjectForKey:@"state"];
    _city = [decoder decodeObjectForKey:@"city"];
    _other = [decoder decodeObjectForKey:@"other"];
  }
  return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder
{
  [encoder encodeObject:zipCode forKey:@"zipCode"];
  [encoder encodeObject:state forKey:@"state"];
  [encoder encodeObject:city forKey:@"city"];
  [encoder encodeObject:other forKey:@"other"];
}
- (void)dealloc
{
  self.zipCode = nil;
  self.state = nil;
  self.city = nil;
  self.other = nil;
}
@end

これで準備は完了です。
それでは Person クラスと Address クラスのオブジェクトを保存してみましょう。
以下の山田太郎さん花子さん、田中次郎さんの3人の個人情報を保存するプログラムを作成します。
Personクラスオブジェクト図
以下はその例です。

Person *tYamada = [[Person alloc] init];
tYamada.name = @"山田太郎";
Address *yAddress = [[Address alloc] init];
yAddress.zipCode = @"104-0061";
yAddress.state = @"東京都";
yAddress.city = @"中央区";
yAddress.other = @"銀座1丁目";
tYamada.address = yAddress;
    
Person *hYamada = [[Person alloc] init];
hYamada.name = @"山田花子";
hYamada.address = yAddress;
    
Person *tanaka = [[Person alloc] init];
tanaka.name = @"田中次郎";
Address *tAddress = [[Address alloc] init];
tAddress.zipCode = @"145-0071";
tAddress.state = @"東京都";
tAddress.city = @"大田区";
tAddress.other = @"田園調布1丁目";
tanaka.address = tAddress;
NSArray *array = @[tYamada, hYamada, tanaka];
BOOL successful = [NSKeyedArchiver archiveRootObject:array toFile:filePath];
if (successful) {
  NSLog(@"%@", @"データの保存に成功しました。");
}

プログラムの実行結果は以下のようになります。

2011-09-04 21:51:55.597 DataManagement[10053:707] データの保存に成功しました。

ファイルに保存された自作クラスのオブジェクトを復元する

それではファイルに保存した Person クラスと Address クラスのオブジェクトを復元してみましょう。

NSArray *array = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
if (array) {
  for (Person *person in array) {
    NSLog(@"%@", person.name);
    NSLog(@"%@", person.address.zipCode);
    NSLog(@"%@", person.address.state);
    NSLog(@"%@", person.address.city);
    NSLog(@"%@", person.address.other);
  }
} else {
  NSLog(@"%@", @"データが存在しません。");
}

プログラムの実行結果は以下のようになります。

2011-09-04 21:51:57.073 DataManagement[10053:707] 山田太郎
2011-09-04 21:51:57.077 DataManagement[10053:707] 104-0061
2011-09-04 21:51:57.079 DataManagement[10053:707] 東京都
2011-09-04 21:51:57.082 DataManagement[10053:707] 中央区
2011-09-04 21:51:57.084 DataManagement[10053:707] 銀座1丁目
2011-09-04 21:51:57.087 DataManagement[10053:707] 山田花子
2011-09-04 21:51:57.089 DataManagement[10053:707] 104-0061
2011-09-04 21:51:57.095 DataManagement[10053:707] 東京都
2011-09-04 21:51:57.098 DataManagement[10053:707] 中央区
2011-09-04 21:51:57.100 DataManagement[10053:707] 銀座1丁目
2011-09-04 21:51:57.103 DataManagement[10053:707] 田中次郎
2011-09-04 21:51:57.106 DataManagement[10053:707] 145-0071
2011-09-04 21:51:57.115 DataManagement[10053:707] 東京都
2011-09-04 21:51:57.119 DataManagement[10053:707] 大田区
2011-09-04 21:51:57.123 DataManagement[10053:707] 田園調布1丁目

改訂履歴

2012年8月2日 記事内容を見直し修正。Modan Objective-C Syntax に対応

サンプルプログラム

記事で使用したサンプルプログラムを以下に置いておきます。