A Day In The Life

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

iOS でデータを永続化する方法

iOS データ設計入門の続きです。前回は iOS であつかうデータ全般について書きましたが今回はデータをフラッシュドライブに保存する方法について説明します。

データの永続化って何?

メモリにあるデータはアプリを終了すると消えてしまいます。
アプリを終了しても残しておきたいデータはフラッシュドライブに保存する必要があります。メモリにあるフラッシュドライブに保存することをデータの永続化といいます。永続化されたデータはフラッシュドライブが壊れない限り永続的に保存され残ります。以降 iOS でフラッシュドライブがどのように管理されているのかと、データを永続化するのにどのような方法があるのかについて説明していきます。

フラッシュドライブを構成する3つの領域

データを永続化する方法を説明する前に iOS でフラッシュドライブがどのように管理されているか見ていきましょう。
iOS ではフラッシュドライブは大きくわけて以下の3つの領域に分けて管理されています*1

  • アプリ領域
    アプリが自由に使うことが出来る領域。アプリごとに割り当てられていて他のアプリの領域を参照することは出来ない。
  • OS 領域
    OS が使用するデータが保存されている領域。OS 本体のファイルやアプリなど、基本的にアプリから参照することは出来ない。
  • 共有領域
    メディアライブラリ(写真、ビデオ、音楽)やイベント、連絡先など、すべてのアプリから参照することができる領域。ただしアプリから共有領域に書き込めるデータは以下の3種類に制限されている。

    種類 説明
    写真および動画 カメラで撮った画像データおよび動画
    UIImagePickerController や AssetsLibrary を使ってデータの参照と書き込みができる
    連絡先 電話帳のデータ
    Address Book を使ってデータの参照と書き込みができる
    イベント カレンダーにひもづいたイベントのデータ
    Event Kit を使ってデータの参照と書き込みができる


共有データとアプリ固有データの関係を図にすると以下のようになります。

共有データにデータを保存することも広い意味でデータの永続化になるのですが、使用頻度や用途が限定的であるため本記事では割愛します。詳細は以下を参照してください。

またアプリ間でデータを共有する方法としてペーストボードを使う方法があります。ペーストボードはメモリを使ってデータを共有する方法なので本記事では割愛します。詳細は以下を参照してください。

アプリ固有領域にデータを永続化する方法

アプリ固有領域にデータを永続化する主な方法は以下の通りです。

  • オブジェクトアーカイブ
    オブジェクトをバイナリ形式に変換してからファイルに永続化する。一般的にはオブジェクトシリアライズと呼ばれる。使用頻度は少ないが SDK の中で頻繁に使われている。データ永続化の中で一番基礎的な技術。
  • プロパティリスト
    プロパティリストと呼ばれる方式でデータを永続化する
    Property List Editor を使って簡単にデータの編集ができるのが特徴
  • NSUserDefaults
    アプリ固有の設定値を永続化することに特化したライブラリ。使用頻度は一番高い
  • Core Data
    Core Data と言われる O/R マッピングフレームワークを使用してデータを永続化する方法
    難易度が高いが使いこなせるようになると条件指定をしてデータを取得したりデータの一意性保証や Undo Redo などができるようになる。データは SQLite に保存される。

各永続化方法には、データの種類によって保存に向いている場合と向いていない場合があります。それを表にまとめると以下のようになります。

定数データ アプリの設定値 モデルオブジェクト
アーカイブ × × △(データ量少)
プロパティリスト △(ユーザが変更しないもの) ×
NSUserDefaults × ○(ユーザが変更するもの) ×
Core Data × × ○(データ量多)

※○…向いている △…特定条件のみ向いている ×…向いていない

iOS ではデータを保存できる場所が決められている

NSUserDefaults と Core Data はプログラマがデータの保存場所を意識する必要がありませんがオブジェクトアーカイビングとプロパティリストはプログラマがデータの保存場所を自分で管理する必要があります。
iOS でファイルを保存できる場所はセキュリティの関係上、各アプリのホームディレクトリ(/Applications//)以下に限られています。
ホームディレクトリは NSHomeDirectory() 関数で取得することができます。

NSLog(@"%@", NSHomeDirectory());

出力結果はこんな感じです。

/var/mobile/Applications/A11CE300-5F63-4918-B46C-2DFA6E18E0B7

A11 に続く文字列が GUID です。GUID はアプリによって変わります。
また iOS ではホームディレクトリ以下に専用のディレクトリがありそれぞれ役割が決まっています。

  • /アプリ名.app
    メインバンドルと呼ばれている。アプリのリソースファイルを保存するためのディレクトリ。読み取り専用。
  • /Documents
    アプリがファイルを作成して保存することができるディレクトリ。永続化したデータを格納する場合ここを使うのが一般的
  • /Library/Caches
    アプリが一時的に使う情報を保存するディレクトリ
  • /Library/Preferences
    アプリケーションの設定を保存するディレクトリ。NSUserDefaults のデータが保存される
  • /tmp
    一時ファイルを保存するディレクトリ。アプリが動作してないときに消される可能性がある

各ディレクトリのパスの取得方法は以下になります。

// アプリ名.app
NSLog(@"%@", [[NSBundle mainBundle] bundlePath]);
// Documents 第2引数と第3引数は固定
NSArray *paths = NSSearchPathForDirectoriesInDomains(
                                NSDocumentDirectory, 
                                NSUserDomainMask, YES);
NSLog(@"%@", paths[0]);
// Library/caches 第2引数と第3引数は固定
NSArray *paths2 = NSSearchPathForDirectoriesInDomains(
                                NSCachesDirectory, 
                                NSUserDomainMask, YES);
NSLog(@"%@", paths2[0]);
// tmp
NSLog(@"%@", NSTemporaryDirectory());

/Library/Preferences は NSUserDefaults が使うディレクトリなのでファイルパスの取得方法はありません。ここに直接アクセスしたくなるようなことはほとんどないと思います。
iOS でアクセスできるディレクトリとそのパスの取得方法を図にまとめるとこのようになります。
ディレクトリ構造

iOS 5からデータの保存場所についてガイドラインが追加されました

iOS 5から「The iOS Data Store Guidelines specify」というデータの保存場所についてのガイドラインが追加されました。このガイドラインに違反しているアプリはリジェクトされるので気をつけてください。iCloud 関連の規約変更だと思います。

The iOS Data Store Guidelines specify:

1. Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the /Documents directory and will be automatically backed up by iCloud.

2. Data that can be downloaded again or regenerated should be stored in the /Library/Caches directory. Examples of files you should put in the Caches directory include database cache files and downloadable content, such as that used by magazine, newspaper, and map applications.

3. Data that is used only temporarily should be stored in the /tmp directory. Although these files are not backed up to iCloud, remember to delete those files when you are done with them so that they do not continue to consume space on the user’s device.

要約するとこんな感じです。

  • /Documents ディレクトリにはユーザが自分の意思で保存したデータを保存すること
  • /Library/Caches ディレクトリには、あとから再びダウンロードして復旧可能なデータを置くこと
  • /tmp ディレクトリには一時的に使用するデータを保存すること

事例として、とあるアプリでサーバからダウンロードした設定情報を NSUserDefaults に保存していたらリジェクトされました。最終的にはサーバからダウンロードしたデータをメモリ上に保存するように修正して対応しました。

永続化方法の具体例

個々の永続化方法について別記事で説明していきます。

参考書籍

*1:3つの領域と書きましたが、実際に領域が物理的に分かれているわけではなく、データの管理方法として3つにわけて管理されているという意味です

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 に対応

サンプルプログラム

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