ラベル Core Data の投稿を表示しています。 すべての投稿を表示
ラベル Core Data の投稿を表示しています。 すべての投稿を表示

CoreData - マイグレーションを考慮した CoreDataManager パターン

2011年2月26日土曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

2/27 マイグレーションにかかる時間を訂正・加筆しました(別の数字を誤って掲載してました。実際はもっと遅い)。

一般的な CoreDataManager パターンの問題


CoreData を使う場合 CoreDataManager というシングルトンを使って NSManagedObjectContext などを管理させるのが一般的なパターン。こんな感じ。
@interface CoreDataManager : NSObject {

 // Core Data Stack
 NSPersistentStoreCoordinator *persistentStoreCoordinator_;
  NSManagedObjectModel *managedObjectModel_;
  NSManagedObjectContext *managedObjectContext_;
}
@property (nonatomic,retain, readonly) NSPersistentStoreCoordinator* persistentStoreCoordinator;
@property (nonatomic,retain, readonly) NSManagedObjectModel* managedObjectModel;
@property (nonatomic,retain, readonly) NSManagedObjectContext* managedObjectContext;

+ (CoreDataManager*)sharedManager;
利用側のコードでは必要な時にここから NSManagedObjectContext を取り出せば良い。
NSManagedObjectContext* moc = [[CoreDataManager sharedManager] managedObjectContext];

ただこれだとエンティティに変更を入れてマイグレーションが必要になった時に困ることがある。例えば起動直後の画面で CoreData へアクセスするような構成で起動時にマイグレーションが発生した場合、起動ルール(20秒)にかかりアプリが起動に失敗するというケースがある。

Cocoaの日々: [iOS] 起動に時間がかかりすぎるとクラッシュする(原因と対策など)

CoreData のマイグレーションは非常に遅くて下図ぐらいのエンティティ構成でレコードが 1,200件程度(親テーブル 100件、子テーブル 12件)のデータの場合、40〜50秒 2分かかる(iOS 4.2.1/3GS、自動マイグレーション、変更はカラム2個追加)。

2/26追記:マイグレーションにかかる時間(上記と同条件)
240件  28秒
600件  1分
1,200件 2分
2,400  2分20秒
4,800  9分
※ケーブルで MacBookに接続した iPhone 3GS に Xcode経由で実行(デバッガ未使用)。


CoreData を使うアプリであればこの程度の件数はすぐに行くので、起動時にマイグレーションが走ると確実に落ちてしまう。これを防ぐためには起動時に CoreData へアクセスさせないのが最低限の対策になるが、その場合でもユーザが CoreData へアクセスする操作を行った瞬間にマイグレーション処理に時間がかかって画面が固まったようになるのでユーザビリティは良くない。


マイグレーションを考慮したパターン


よって CoreDataを使うアプリではマイグレーション用の画面を用意するのがベスト。処理フローはこんな感じ。
起動
 ↓
(1)マイグレーションチェック
 もし必要なら、マイグレーション用の画面へ遷移し、(2)マイグレーション実行
 ↓
通常画面
マイグレーションチェックは NSPersistentCoordinator を使えばわかる。

Cocoaの日々: [iOS][Mac] CoreData - マイグレーションが必要かどうかを知る

以下は実際に上記パターンを適用した時の画面例。
アプリ起動後にマイグレーションが必要と判断したら専用の画面をモーダル表示させる。
if ([manager isRequiredMigration]) {
   // do migration
   MigrationViewController* viewController = [[MigrationViewController alloc] init];
   viewController.rootViewController = self;
   viewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
   [self presentModalViewController:viewController animated:NO];
   [viewController release];
 :
表示した画面の中でマイグレーションを実行する。画面には UIActivityIndicatorView を表示して回しておく。iOS 4 以降であれば GCD が使えるのでこの辺りの処理は簡単に書ける。
- (void)viewWillAppear:(BOOL)animated
{
 dispatch_queue_t queue =
  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

 self.okButton.hidden = YES;
 self.buttonLabel.hidden = YES;
 self.indicator.hidden = NO;
 self.warningLabel.hidden = NO;
 
 dispatch_async(queue, ^{
  [[CoreDataManager sharedManager] doMigration];

  dispatch_async(dispatch_get_main_queue(), ^{
   self.message.text = @"バージョンアップが完了しました";
   self.indicator.hidden = YES;
   self.okButton.hidden = NO;
   self.buttonLabel.hidden = NO;
   self.warningLabel.hidden = YES;
  });
 });

}
マイグレーションが終わったらOKボタンを表示する。ここはアプリによってはそのまま通常画面へ遷移しても良い。


上記のように CoreDataManager にはマイグレーション用のメソッドが必要。こんな感じ。
@interface CoreDataManager : NSObject {

 // Core Data Stack
 NSPersistentStoreCoordinator *persistentStoreCoordinator_;
  NSManagedObjectModel *managedObjectModel_;
  NSManagedObjectContext *managedObjectContext_;
}
@property (nonatomic,retain, readonly) NSPersistentStoreCoordinator* persistentStoreCoordinator;
@property (nonatomic,retain, readonly) NSManagedObjectModel* managedObjectModel;
@property (nonatomic,retain, readonly) NSManagedObjectContext* managedObjectContext;

+ (CoreDataManager*)sharedManager;

// for migration
- (BOOL)isRequiredMigration;
- (BOOL)doMigration;
自動マイグレーションを使っている場合、マイグレーションが発生するタイミングは NSpersistentCoordinator にマイグレーションオプションを設定した上で -addPersistentStoreWithType:configuration:URL:options:error: を投げた時になる。以下は一般的な -psersistentStoreCoordinator のコード例。
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
 if (persistentStoreCoordinator_ != nil) {
        return persistentStoreCoordinator_;
    }

    persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc]
          initWithManagedObjectModel:self.managedObjectModel];
 
 NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
        [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
        [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
  
    NSError *error = nil;
 NSURL* fileURL = [CoreDataManager fileURL_]; 
    if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType
              configuration:nil
               URL:fileURL
              options:options
                error:&error]) {
  NSLog(@"Creating persistentStoreCoordinator was failed (%@, %@)", error, [error userInfo]);
  abort();
    }

    return persistentStoreCoordinator_;
}
NSMigratePersistentStoresAutomaticallyOption と NSInferMappingModelAutomaticallyOption がマイグレーションオプション。通常の -persistentStoreCoordinator メソッドではこのオプションを設定せず、別途用意する -doMigration でマイグレーション専用の NSPersistentCoordinator を用意しそこでオプションを設定してマイグレーションを実行(-addPersistentStoreWithType:configuration:URL:options:error:)すれば良い。マイグレーション実行後はここで作った NSPersistentCoordinator は捨てて良い(NSPersistentCoordinator は同じDBファイルに対して複数作成可能)。

なお CoreData へのアクセスタイミングをきちんと制御できるのであれば別途 -doMigration を用意せず既存のメソッドだけでも良い( [[CoreDataManager sharedManager] managedObjectContext] が最初に呼ばれたタイミングで -psersistentStoreCoordinator も呼ばれるのでそこでマイグレーションが走る)。

- - - -
CoreData を扱う上での一番のネックはマイグレーションに時間がかかること。たった一つのカラム追加でも現状は全データの移行作業が発生するので(特に iOSでは)この時間が馬鹿にならない。CoreDataはプログラミング的には非常に便利なのだがこの運用時のマイグレーション処理が少々厄介。アプリで CoreData を採用するかどうか判断する時にはこの点を考慮した方が良いかと思う。


参考情報


Cocoaの日々: [iOS] CoreData - マイグレーション[5] マイグレーション中にアプリを終了させたらどうなる?

[iOS][Mac] CoreData - マイグレーションが必要かどうかを知る

2011年2月24日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

マイグレーションの要不要は?


CoreData では属性を追加したり変更するとマイグレーションが必要になる。過去にリリースしたアプリを新しいアプリでバージョンアップする時にマイグレーションが必要かどうか判断するにはどうしたらよいか?


実装


NSPersistentStoreCoordinator を使えば良い。
こんな感じ。
- (BOOL)isRequiredMigration
{
 CoreDataManager* manager = [CoreDataManager sharedManager];
 [[[NSPersistentStoreCoordinator alloc]
  initWithManagedObjectModel:manager.managedObjectModel] autorelease];
 
 NSURL* fileURL = [CoreDataManager fileURL_];
 NSError* error = nil;
 
 NSDictionary* sourceMetaData =
  [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
                   URL:fileURL
                 error:&error];
 
 if (sourceMetaData == nil) {
  return NO;
 } else if (error) {
  NSLog(@"Checking migration was failed (%@, %@)", error, [error userInfo]);
  abort();
 }
 

 BOOL isCompatible = [manager.managedObjectModel isConfiguration:nil
          compatibleWithStoreMetadata:sourceMetaData]; 

 return !isCompatible;
 
}
古いバージョンのアプリで使用しているメタデータと、(これから実行しようとしている)新しいアプリで使用するメタデータの比較を行えば良い。



- - - -
NSPersistentStoreCoordinator のインスタンスは1つしか作れないという制限はなく必要ならいくつでも作ることができる。これは同じDBファイルに対してもそう(ただし非スレッドセーフなので排他制御は自分で行う必要がある。その為のメソッドも用意されている)。
上記の様にマイグレーションの要不要をチェックする為に NSPersistentStoreCoordinator のインスタンスを作ることは、複数インスタンスを作る場合の利点の一つ。普通に1つの NSPersistentStoreCoordinator インスタンスだけで通常の利用とマイグレーションを一緒にやろうとした場合、起動時にDBへアクセスしようとした時に時間のかかるマイグレーションが発生して起動時間問題にひっかかるなどの弊害が出る場合がある。
他には大量のインポートを行いたい時に専用の NSPersistentStoreCoordinator インスタンスを用意する、複数のスレッドでインスタンスを持つ、などが考えられる。SQLite に対するオプションは NSPersistentStoreCoordinator の単位で設定できるので、インポートなど特殊な用途向けのオプションを設定する場合などにインスタンスを分けることが役立つ。

[Mac][iOS] NSPredicate - SUBQUERY の書き方

2011年1月19日水曜日 | Published in | 4 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本(訂正〜サブクエリーの利用)

SUBQUERY情報


前回使った SUBQUERY の情報は NSPredicate の話題を扱っている "Predicate Programming Guide" には無くて、NSExpression のクラスリファレンスに記載されていた。

NSExpression Class Reference

書式はこう
SUBQUERY(collection_expression, variable_expression, predicate);
variable_expression は predicate 内で collection_expression を参照する時の名前を記述する。
前回の設定を例に出すとこんな感じ。
SUBQUERY(books, $s, $s.date >= %@ AND $s.date < %@).@count > 0
$s が books の(一種)別名となり、これを条件内で $s として参照している。SUBQUERYの結果に対して集計関数を適用することができる。例では @count(レコード数)関数を適用して「0件以上」という条件にしている。

[参考情報] Cocoaの日々: [Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本(集計関数使用)


以下、リファレンスページから例文を転載して紹介する。
(SUBQUERY(residents, $x, $x.firstname == "Jane" && $x.lastname == "Doe").@count != 0)
こうも書けるらしい。
(SUBQUERY(residents, $x, $x.firstname == "Jane" && $x.lastname == "Doe")[size] != 0)


参考情報


Predicate Programming Guide: Introduction to Predicates Programming Guide

.

[Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本(訂正〜サブクエリーの利用)

2011年1月18日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本

先日の見本が間違っていることが発覚。先日のやつはこんな指定だった。
p = [NSPredicate predicateWithFormat:
  @"ANY books.date >= %@ AND ANY books.date < %@", date1, date2]
この時の SQLはこうなる。
SELECT DISTINCT 0, t0.Z_PK FROM ZAUTHOR t0
 JOIN ZBOOK t1 ON t0.Z_PK = t1.ZAUTHOR
 JOIN ZBOOK t2 ON t0.Z_PK = t2.ZAUTHOR
 WHERE ( t1.ZDATE >= ? AND  t2.ZDATE < ?)
よくよく考えると同じテーブルではあるが、別個に結合(JOIN)しているので、WHERE句の AND 条件が意図通りに働かない。実際、テストしていて意図しないレコードがヒットするのに気がついた。これは WHERE句内の2つの条件が(元は同じだが)別のテーブル(集合)として扱われる為。 ただ先日示したように ANY(条件 AND 条件) は使えない(パースエラーになる)。この場合 SUBQUERY を使う。SUBQUERYを使って書くと先ほどの設定はこうなる。
p = [NSPredicate predicateWithFormat:
 @"SUBQUERY(books, $s, $s.date >= %@ AND $s.date < %@).@count > 0", date1, date2];
SQLはこうなる。
SELECT 0, t0.Z_PK FROM ZCUSTOMER t0
 WHERE (SELECT COUNT(*) FROM ZKARTE t1
  WHERE (t0.Z_PK = t1.ZCUSTOMER
  AND (( t1.ZTREATEDDATE >= ? AND  t1.ZTREATEDDATE < ?))) ) > ? 
これなら問題ない。実際の動作でも意図通りとなった。


参考情報


iphone - Core Data ANY BETWEEN predicate - Stack Overflow
まったく同じ問題で困っていた人がいた。

[Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本(集計関数使用)

2011年1月13日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本

お題


今回は最も新しい本の発売日が指定した日以前の著者一覧を取得する、という条件を作ってみる。例えば 2010年10月2日を指定した場合、最も新しい本が 10月2日以前の著者が該当する。11月1日に本を発売している著者はヒットしない。


条件見本


"最も新しい" 日付を条件に使うために max() 関数を使う。
NSPredicate* p = [NSPredicate predicateWithFormat:@"max(books.date) <= %@", date];
集計関数 max() を使うので ANY は必要ない。 発行される SQLはこんな感じ。
SELECT 0, t0.Z_PK FROM ZAUTHOR t0 WHERE
 (SELECT MAX(t1.ZDATE)  FROM ZBOOK t1
   WHERE (t0.Z_PK = t1.ZAUTHOR) ) <= ?
サブクエリーで子テーブルBOOKから最も新しい日付を抽出し、それと条件を比較している。

なお NSPredicate には NONE というキーワードも用意されている。
Predicate Programming Guide: Predicate Format String Syntax

これを使うと下記のようにも書けそうである。
NSPredicate* p = [NSPredicate predicateWithFormat:@"NONE books.date > %@"];
指定日時よりも新しい発売日が無いと意味(=最も新しい発売日が指定日時以前)。
ただ実際には意図通りには動かない。SQLを見ると単純に NOT が付くだけのようだ。
SELECT DISTINCT 0, t0.Z_PK FROM ZAUTHOR t0
  JOIN ZBOOK t1 ON t0.Z_PK = t1.ZAUTHOR
  WHERE  NOT (t1.DATE > ?)


NSPredicate 内で利用可能な関数


NSPredicate で使える関数は max()の他、count, min, sum などがある。 以下、Predicate Programming Guide: BNF Definition of Cocoa Predicates の Function Name から転載。
function_name ::= "sum" | "count" | "min" | "max"
    | "average" | "median" | "mode" | "stddev"
    | "sqrt" | "log" | "ln" | "exp"
    | "floor" | "ceiling" | "abs" | "trunc"
    | "random" | "randomn" | "now"
count, min などの集計関数の他、sqrt, log など数値変換用の関数も用意されている。

[Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本

2011年1月11日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

2011-01-20 追記
ここに書かれている方法には誤りがあります。正しい方法を別途書きましたので、そちらも合わせて読んで下さい。
Cocoaの日々: [Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本(訂正〜サブクエリーの利用)


お題


こんな1対多のエンティティがあったとする。
発売日(Book.date)が指定日以降の本を書いた著者(Author)の一覧を取得したい。この時の条件(NSPredicate)はどう書くべきか。


条件見本


NSPredicate ではキーパスを指定できるので日付の条件はこう書ける。
books.date <= %@
Author に対してこの条件を投げるわけだが、今回結果として欲しいのは著者の一覧なので該当する Bookが存在する Authorだけが帰ってくるようにしたい。この場合は ANY をつけてやる。
ANY books.date <= %@
Author を対象とした検索条件 NSPredicate はこうなる。
NSPredicate* p = [NSPredicate predicateWithFormat:@"ANY books.date <= %@", date];
実際に発行されているSQL文を確認してみよう。
SELECT DISTINCT 0, t0.Z_PK FROM ZAUTHOR t0
  JOIN ZBOOK t1 ON t0.Z_PK = t1.ZAUTHOR
  WHERE t1.ZDATE >= ?
親子のテーブルをJOINして、条件にマッチしたレコードをDISTINCTを使って重複をなくしている。意図通りだ。SQLのEXISTSを使えばもう少し効率を上げられそうではあるが件数が多くなければこれでも十分。


条件が複数


もう一つ条件を加えて、ある期間内に発売された本を書いた著者の一覧を取得する。単純に条件を加えてみるとこんな感じ。
ANY (books.date >= %@ AND books.date < %@)
これは実はNG。NSInvalidArgumentException (Unsupported predicate) が発生する。正解はこう。
ANY books.date >= %@ AND ANY books.date < %@
ANY をそれぞれの頭につけてやる。

SQLはこうなる。
SELECT DISTINCT 0, t0.Z_PK FROM ZAUTHOR t0
 JOIN ZBOOK t1 ON t0.Z_PK = t1.ZAUTHOR
 JOIN ZBOOK t2 ON t0.Z_PK = t2.ZAUTHOR
 WHERE ( t1.ZDATE >= ? AND  t2.ZDATE < ?)
2回 JOINをしているのがアレだが、まあ意図通りの動きにはなるならない。→ 冒頭に記載された最新情報を参照してください。


備考


範囲条件指定には BETWEEN というのもある。
(例)
NSNumber *one = [NSNumber numberWithInteger:1];
NSNumber *ten = [NSNumber numberWithInteger:10];
NSPredicate *betweenPredicate = [NSPredicate predicateWithFormat:
     @"attributeName BETWEEN %@",
     [NSArray arrayWithObjects:one, ten, nil]];
ただ NSDate を条件にした場合例外が出てしまった。使い方がまずいのか仕様なのか。

.

[Mac][iOS] NSCompoundPredicate で条件をまとめる

2011年1月10日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

Core Data を使ったアプリケーションで下のような検索機能を実装している。

設定された値を元に NSPredicate を作成し、Core Data に対して検索をかけるのだが、こういう場合に NSCompoundPredicate が役に立つ。
NSCompoundPredicate Class Reference

各々の条件は必ずしも指定されるわけではないので検索条件の項目は可変となる。普通の NSPredicate を使う場合、条件の有無によって文字列の連結を使って条件を構成する必要がある。こんな感じ。
NSMutableString* fmt = [NSMutableString string];
NSMutableArray* array = [NSMutableArray array];
if (name) {
    [fmt appendString:@"name == %@"];
    [array addObject:model.name];
}
if (customerType) {
    if ([fmt lenght] > 0) {
        [fmt appendString:@" AND "];
    }
    [fmt appendString:@"customerType == %@"];
    [array addObject:model.customerType];
  :

NSPredicate* predicate = [NSPredicate predicateWithFormat:fmt argumentArray:array];
これはこれで動くのだが NSCompoundPredicate を使うともっと簡潔に書ける。こんな感じ。

NSMutableArray* array = [NSMutableArray array];
if (name) {
    [array addObject:[NSPredicate predicateWithFormat:@"name == %@", model.name]];
}
if (customerType) {
    [array addObject:[NSPredicate predicateWithFormat:@"customerType == %@",
        model.customerType]];
}
  :
NSPredicate* predicate = [NSCompoundPredicate andPredicateWithSubpredicates:array];
上記は AND接続だったが他に OR, NOT が用意されている。

(a AND b) OR (c AND b) とやりたい場合は NSCompundPredicate を複数使えば良い。
NSPredicate* a, b, c, d;
a = ....;
b = ....;
c = ....;
d = ....;
NSPredicate* r1, r2, r3;
r1 = [NSPridicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:a, b, nil]];
r2 = [NSPridicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:c, d, nil]];
r3 = [NSPridicate orPredicateWithSubpredicates:[NSArray arrayWithObjects:r1, r2, nil]];
条件内のカッコ( ) は自動的に付く。組み立てた条件は NSLogで NSPredicate を表示すれば確認できる。

notPredicateWithSubpredicate: を使った場合、頭に NOT が付く。
NSPredicate* a = [NSPredicate predicateWithFormat:@"name == %@", name];
NSPredicate* r = [NSCompoundPredicate noPredicateWithSubpredicate:a];
この時 r は NOT (name == @"hogehoge") となる。

[iOS] CoreData - マイグレーション[5] マイグレーション中にアプリを終了させたらどうなる?

2010年12月14日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [iOS][Mac] CoreData - マイグレーション[4] モデルファイルの構成

マイグレーションの検証中の素朴な疑問:Core Data のマイグレーション中にアプリを落としたり、電源を落としたりするとどうなるのか?
試してみた。


サンプルプログラム


前回のソースに手を入れて初回起動時に 10,000件のデータを登録するようにしてみた。実行するとデバッグコンソールに 20件毎のコミット状況が書きだされる。
最初にバージョン1のモデルで実行し、次にバージョン2のモデルへ切り替えてマイグレーションが実行されるようにする。

まず最初に正常動作の確認をしたところ次の結果となった。
[バージョン1] サンプルデータ 10,000件の生成: 32秒
[バージョン2] マイグレーション 10,000件  : 24秒
※ iPhone 3GS / iOS 4.2

マイグレーションの開始と終了は NSEntityMigrationPolicy のメソッドを利用してわかるようにした。こんな感じ。
- (BOOL)beginEntityMapping:(NSEntityMapping *)mapping
 manager:(NSMigrationManager *)manager error:(NSError **)error
{
 NSLog(@"Begin migration");
 return YES;
}
- (BOOL)endEntityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager
 error:(NSError **)error
{
 NSLog(@"End migration");
 return YES;
}



アプリ終了


次にマイグレーション実行中にアプリケーションを停止させてみた。ログを見ていると終了後も処理は実行されていてマイグレーションが完了した。どうも自動的にバックグラウンドでマイグレーション処理が継続して実行されているようだ。ちなみにサンプルプログラムはバックグラウンド動作の指定はしていない。UIApplicationExitsOnSuspend は True になっている。


アプリ強制終了


Fast App Switching でアプリを強制終了させても動作し続けていた。うーむ。
※この後は検証していない


電源オフ


マイグレーション実行中にアプリを終了させ、さらに電源をオフにしてみた。しかしこの間もバックグラウンドで動き続けていた。10,000件のデータの場合、処理時間が長いのでバックグラウンドの10秒ルールに抵触して強制終了させられてしまった。
SpringBoard[27] <Warning>: CoreDataMigSamp[1241] has active assertions beyond permitted time:
     {(
         <SBProcessAssertion: 0x5c53d80> identifier: Suspending process: CoreDataMigSamp[1241] 
           permittedBackgroundDuration: 10.000000 reason: suspend owner pid:27 preventSuspend
           preventThrottleDownCPU  preventThrottleDownUI
     )}
SpringBoard[27] <Warning>: Forcing crash report of CoreDataMigSamp[1241]...
SpringBoard[27] <Warning>: Finished crash reporting.
ReportCrash[1258] <Error>: Saved crashreport to /var/mobile/Library/Logs/Cra...
Tcom.apple.launchd[1] <Notice>: (UIKitApplication:com.yourcompany.
  CoreDataMigSample[0x11dc]) Exited: Killed
処理件数 3,000件を試した時にはマイグレーション処理が10秒以内に収まったので、この時は電源オフ後にマイグレーションが完了した。

さて途中で Killされた後、アプリを起動するとどうなるのか。
CoreDataMigSample[1389:307] Removed orphaned, partially migrated store file
 file://localhost/var/mobile/Applications/A7DFB71B-5B24-41D4-A871-
 F2AA18369684/Documents/.CoreDataMigSample.sqlite.migrationdestination_
 41b5a6b5c6e848c462a8480cd24caef3
CoreDataMigSample[1389:307] Begin migration
CoreDataMigSample[1389:307] End migration
前回の失敗が破棄されてマイグレーションが最初から実行され、そして完了した。おお、Core Data すごいぜ。。


考察


Core Data のマイグレーションは1箇所の変更であっても、新DB作成→旧DBからのマイグレーションという手続きを取る(新DBはマイグレーション成功後に正式な名前に替えられて旧DBと置き換わる。旧DBは名前が変更されてバックアップとして残る)。その為に件数が多い場合は時間がかかる。このマイグレーションに時間がかかる場合 iOS デバイスだと途中でアプリを終了させられたり電源を落とされる可能性が高い。それ故、Core Dataを使ったアプリのバージョンアップに関する大きな懸念点の一つとしてずーっと気になっていた。これが今回の検証で不整合が基本的に発生しない作りになっていることが確認できたのは大きい。マイグレーションのバックグラウンド動作は iOS 4.0 からなのかは確認できていないが、少なくとも iOS 4.0以上をターゲットにしたアプリケーションで Core Data を使う場合の懸念材料が一つ減った。機能面と合わせ、運用面での性能も充実してきたので iOSデバイスでも十分に実用に耐えるのではないかと思う。


ソースコード


GitHub からどうぞ。
CoreDataMigSample at 2010-12-14 from xcatsan/iOS-Sample-Code - GitHub


- - - -
後はマイグレーション処理中の経過をユーザへフィードバックできればいいな。後ほど調べてみよう。

[iOS][Mac] CoreData - マイグレーション[4] モデルファイルの構成

2010年12月12日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [iOS][Mac] CoreData - マイグレーション[3] エラー

モデルファイルの構成・配置についてのまとめ。


モデルファイルの構成〜Xcode


XcodeでCore Data のモデルを定義すると <modelname>.xcdatamodeld というグループが作られ、その下に <modelname>.xcdatamodel ファイルが配置される。モデルファイルをバージョンアップするとこのグループ内にバージョン毎のモデルファイルが追加されていく。下記はバージョン1とバージョン2が存在する例。


モデルファイルの構成〜実体


<modelname>.xcdatamodeld の実体はパッケージ(フォルダ)である。
Finderで見るとこう。
パッケージの中身を見ると <modelname-version>.xcdatamodel が格納されているのがわかる。


モデルファイルの構成〜実行時


これをビルドするとその結果アプリケーションバンドル内には <modelname>.momd というフォルダが作成され、その中に <modelname-version>.mom ファイルが格納される。先ほど例に上げたファイルの場合、HairConcierge.momd というパッケージ内に HairConcierge.mom, HairConcierge 2.mom が格納される。
VersionInfo.plist はバージョン管理情報が格納されている。中身はこんな感じ。
どのバージョンのモデルを使っているのか、またそれぞれのバージョンで使用しているエンティティのリストが格納されている。

なお、複数のバージョンが存在しない場合(一番最初のバージョンなど)は <modelname>.momd は存在せず、バンドルフォルダのルートレベルに <modelname>.mom ファイルが設置される。
(単一、複数で配置場所が変わることがバージョンアップ時の問題を引き起こす。問題の対応方法については前回の記事を参照のこと)


トラブル


先日訳あってモデルファイル名を変更した。具体的には頭大文字を小文字にした。
HairConcierge.xcdatamodeld → hairConcierge.xcdatamodeld
それと同時にモデルファイルをバージョンアップし、マイグレーションの設定を行った。

この新しいバージョンのアプリをビルドして古いバージョンを上書きインストールしたところマイグレーションが行われない現象が出た。原因がわからずいろいろ調べているとアプリケーションバンドル内のモデルファイルが旧バージョンしか存在しないことがわかった。
HairConcierge.momd
         |--HairConcierge.mom
         |--VersionInfo.plist
本来は新しいバージョンのモデルファイル HairConcierge 2.mom が存在するはず。??

さらに調べると momdフォルダが2つ存在することが判明。
HairConcierge.momd
         |--HairConcierge.mom
         |--VersionInfo.plist

hairConcierge.momd
         |--hairConcierge.mom
         |--hairConcierge 2.mom
         |--VersionInfo.plist
原因はなんとモデルファイルの名前を変えたことだった。頭を大文字から小文字に変えたため、旧バージョンのモデルファイルと別扱いになっていた。マイグレーションが実行されなかったのは、2つの momdフォルダのうち旧バージョンだけが含まれる前者を使っていたから。なんてこった。

旧バージョンは既に App Store で配布しているので今回は名前を元に戻すしか無い(大文字に戻す)。実際、名前を元に戻したところ、上記のような2つの momd フォルダは作成されず、無事にマイグレーションが行われた。構成はこんな感じになる。
HairConcierge.momd
         |--HairConcierge.mom
         |--HairConcierge 2.mom
         |--VersionInfo.plist


教訓:リリース後にモデルファイル名を変えてはいけない!

[参考] Cocoaの日々: [iOS] プロダクト名を変えてはいけない

[iOS][Mac] CoreData - マイグレーション[3] エラー

2010年12月8日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

Core Data でマイグレーション設定を行い実行すると次のエラーが出て落ちた。
[14647:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
 reason: 'Can't merge models with two different entities named 'Customer''
*** Call stack at first throw:
(
0   CoreFoundation    0x0150dbe9 __exceptionPreprocess + 185
1   libobjc.A.dylib   0x016625c2 objc_exception_throw + 47
2   CoreData          0x0037a9b9 +[NSManagedObjectModel modelByMergingModels:] + 3865
3   CoreData          0x00378fcb +[NSManagedObjectModel mergedModelFromBundles:] + 507
  :

調べてみると次の記事が見つかった。
iPhone Development: Core Data Migration Problems?

どうも下記のコードが問題のようだ。
- (NSManagedObjectModel *)managedObjectModel
{
if (managedObjectModel_ != nil) {
        returnmanagedObjectModel_;
    }

    managedObjectModel_ = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];

    returnmanagedObjectModel_;
}
上記の -[NSManagedObjectModel mergedModelFromBundles:] が新旧両方のモデルを一緒に読み込もう(merge)としてエラーになっているようだ。実際にアプリケーションパッケージの中を覗いてみると次のようになっていた。

*.momd という名のフォルダが作られてその中に新旧モデルファイル(*.mom)とバージョン管理用のファイルが入っている。-[NSManagedObjectModel mergedModelFromBundles:] はアプリケーションバンドル内のモデルファイル(*.mom)をバージョン管理を無視して読み込む(merge)しようとするようだ。なおマイグレーションを行わない初期の状態ではこのフォルダは存在せず、最初のバージョンのモデルファイル(*.mom)が一つあるだけ。

解決方法はモデルファイルの検索を NSManagedObjectModel に任せないで *.momd フォルダの位置を明示的に指定すれば良い。こんな感じ。
- (NSManagedObjectModel *)managedObjectModel
{
if (managedObjectModel_ != nil) {
        returnmanagedObjectModel_;
    }

    NSString *path = [[NSBundle mainBundle] pathForResource:@"hairConcierge" ofType:@"momd"];
    NSURL *url = [NSURLfileURLWithPath:path];
    managedObjectModel_ = [[NSManagedObjectModelalloc] initWithContentsOfURL:url];

    returnmanagedObjectModel_;
}
上記はブログで紹介されていたのと同等のコードだが、少し改良して momdフォルダ名を書かないコードにしてみた(必要だったので)。こんな感じ。
- (NSManagedObjectModel *)managedObjectModel
{
if (managedObjectModel_ != nil) {
        returnmanagedObjectModel_;
    }

    NSArray* paths = [[NSBundlemainBundle] pathsForResourcesOfType:@"momd" inDirectory:nil];
    NSURL *url = [NSURLfileURLWithPath:[paths objectAtIndex:0]];
    managedObjectModel_ = [[NSManagedObjectModelalloc] initWithContentsOfURL:url];

    returnmanagedObjectModel_;
}

[iOS][Mac] CoreData - マイグレーション[2] 自動マイグレーションで FUNCTIONを使う

2010年12月7日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

前回 NSEntityMigrationPolicy を使ったマイグレーションを紹介した。

[前回] Cocoaの日々: [iOS][Mac] CoreData - マイグレーション[1] NSEntityMigrationPolicy を使う

NSEntityMigrationPolicy を使うことで複雑なマイグレーションを行うことができる。前回は NSDate から月と日を抽出するコードを書いたが、これぐらいであれば自動マイグレーション(Lightweigt Migration)が使えないだろうか。ドキュメントを読み返していると FUNCTION が使えそうなのがわかったのでこれを試してみる。


サンプル


まずは結果から。前回のコードを書き換えて自動マイグレーションに切り替えた。マッピングモデルはこんな感じ。
前回と同じ。後で説明するが NSEntityMigrationPolicy を今回も使う。
day と month の値式に今回は FUNCTION を記述。
FUNCTION($entityPolicy, "monthOfDate:" , $source.timeStamp)
ドキュメントを見つけられなったので推測になるが FUNCTION は最初の引数にメッセージを投げる先のインスタンス、2番目にセレクタ(文字列)、3番目以降がセレクタの引数、といった仕様のようだ。指定したインスタンスへ指定した引数でメッセージを投げ、その結果が移行先の属性値として使われる。

$entityPolicy は「カスタムポリシー」で指定したクラスのインスタンスを指す。今回は NSEntityMigrationPolicy のサブクラス EventEntityMigrationPolicy を指定した。この $ で始まる予約語には次のものがある。
NSMigrationManagerKey: $manager
NSMigrationSourceObjectKey: $source
NSMigrationDestinationObjectKey: $destination
NSMigrationEntityMappingKey: $entityMapping
NSMigrationPropertyMappingKey: $propertyMapping
NSMigrationEntityPolicyKey: $entityPolicy
Core Data Model Versioning and Data Migration Programming Guide: Mapping Overview より転載)

EventEntityMigrationPolicy の実装はこう。
@implementation EventEntityMigrationPolicy

- (NSNumber*)monthOfDate:(NSDate*)date
{
 NSCalendar* calendar = [[[NSCalendar alloc]
    initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
 NSDateComponents* components = [calendar components:NSMonthCalendarUnit|
    NSDayCalendarUnit fromDate:date];
 return [NSNumber numberWithInteger:[components month]];
}
- (NSNumber*)dayOfDate:(NSDate*)date
{ 
 NSCalendar* calendar = [[[NSCalendar alloc]
    initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
 NSDateComponents* components = [calendar components:NSMonthCalendarUnit|
    NSDayCalendarUnit fromDate:date];
 return [NSNumber numberWithInteger:[components day]];
}

前回の create..メソッドはコメントアウトしてある。


備考


FUNCTION のターゲットオブジェクト


Event という名前の NSManagedObjectContext をサブクラスを用意して、そこに -dayOfDate: -monthOfDate: を実装したが、実行時にはエラーとなった。
e[8858:207] -[NSManagedObject monthOfDate:]: unrecognized selector sent to instance 0x4d87c10
[8858:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
 reason: '-[NSManagedObject monthOfDate:]: unrecognized selector sent to instance 0x4d87c10'

モデル定義でクラス指定しても駄目だった。
本来はここにマイグレーションコードが書けるといいのだが(NSEntityMigrationPolicyのインスタンスを用意する必要がなくなる)。


属性マッピングの値式に文字列を指定

属性マッピングの値式に文字列を書くときは ""で囲う。
◯ "Hello" × Hello ※ null扱いになる



ソースコード


GitHub からどうぞ。
xcatsan/iOS-Sample-Code at 2010-12-07 - GitHub

[iOS][Mac] CoreData - マイグレーション[1] NSEntityMigrationPolicy を使う

2010年12月6日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

以前、CoreData のマイグレーションについて調べたことがある。
(旧) Cocoaの日々: CoreData - マイグレーション

これはマッピングモデルを定義するだけで簡単にできる、いわゆる「自動マイグレーション」を解説していた。
今回は NSEntityMigrationPolicy を使ったいわゆる「手動マイグレーション」について調べた。


マイグレーションの概要


Core Data ではエンティティ(テーブル)の定義を変更すると必ずマイグレーションを行う必要がある。例えば現在のアプリケーションをバージョンアップする際に属性 email2 を一つ追加する場合、マイグレーション設定を行わないと Core Data 利用時に例外が発生してアプリケーションが落ちてしまう。
(例) CoreDataMigSample[8394:207] Unresolved error
 Error Domain=NSCocoaErrorDomain Code=134100 "The operation couldn’t be completed. (Cocoa error 134100.)"
 UserInfo=0x581be80 {metadata={type = immutable dict, count = 7,
    :
}
, reason=The model used to open the store is incompatible with the one used to create the store}, {
    metadata =     {
    :

マイグレーションを行うには2つの方法がある。

 1. Lightweight Migration(いわゆる自動マイグレーション)
 2. NSEntityMigrationPolicy利用(いわゆる手動マイグレーション)



1. の方法は Xcode でマッピングモデルを用意するだけでマイグレーションの設定ができる。属性の追加など簡易なものであればこの方法で最も簡単にマイグレーションを実現できる。
(詳しくは「(旧) Cocoaの日々: CoreData - マイグレーション」まで)

2. の方法は NSEntityMigrationPolicy のサブクラスを作成してマイグレーションに必要な実装をコーディングする。今回紹介するのは 2. の方法。


NSEntityMigrationPolicy を利用したマイグレーション


マイグレーションプロセスは "Three-Stage Migration" と呼ばれる。Core Data 利用時にバージョンの差異が検出されるとこのマイグレーションプロセスが実行される。
1. Creation Stage(属性のマイグレーション)
2. Relationship Creation Stage(関連のマイグレーション)
3. Validation Stage(検証ステージ)

NSEntityMigrationPolicy のサブクラスを用意して設定しておくと、このマイグレーションプロセスの中で NSEntityMigrationPolicy の各メソッドが呼び出されていく。サブクラスで必要に応じてこれらのメソッドをオーバーライドしてマイグレーションコードを記述する。

NSEntityMigrationPolicy Class Reference

メソッドの呼び出し順序は次の通り。
[1] – beginEntityMapping:manager:error: 

== Creation Stage ==
[2] – createDestinationInstancesForSourceInstance:entityMapping:manager:error:
    ⇒ インスタンス(NSManagedObject)の数だけ繰り返し呼び出される
[3] – endInstanceCreationForEntityMapping:manager:error:

== Relationship creation stage ==
[4] – createRelationshipsForDestinationInstance:entityMapping:manager:error:
   ⇒ インスタンス(NSManagedObject)の数だけ繰り返し呼び出される
[5] – endRelationshipCreationForEntityMapping:manager:error:

== Validation stage ==
[6] – performCustomValidationForEntityMapping:manager:error:
==
[7] – endEntityMapping:manager:error:
マイグレーション対象のエンティティ毎に NSEntityMigrationPolicy のサブクラスを定義し、マイグレーションプロセスに適した実装を行って行くのがこの方法のスタイルとなる。

なおマイグレーションプロセス自体をカスタマイズすることができる。詳しくは下記を参考のこと。
Core Data Model Versioning and Data Migration Programming Guide: Customizing the Migration Process


サンプル


実際に NSEntityMigrationPolicy を使ったマイグレーションのサンプルプログラムを作ってみる。

まず Xcodeのテンプレートから "Navigation-based Application" を選びオプション "Use Core Data for storage" を付けて新規プロジェクトを作成する。ビルドすると[+]ボタンを押した時のタイムスタンプを Core Data へ格納してテーブル表示するアプリが動く。
エンティティはこんな感じ。
このエンティティへカラムを追加する。day, memo, month を追加してみよう。
day と month は timeStamp の月と日の値(整数)を設定する。

まず新しいバージョンのモデルを作成する。現在のモデルファイル(*.xcdatamodel)を選択してメニュー「設計」→「データモデル」→「モデルバージョンを追加」を選ぶ。すると新しいバージョンのモデルファイルが作成する。
このファイルを開き、先ほどのカラムを追加する。

次にマッピングモデルファイルを作成する。新規ファイル作成で "Mapping Model" を選ぶ。
新旧のモデルファイルを指定すると、マッピングモデルファイルが生成される。ここでは 1to2.xcmappingmodel という名前にしてみた。マッピングモデルファイルを開くと、エンティティ毎のマッピング設定が表示される。
カスタムポリシーに NSEntityMigrationPolicy のサブクラス名を設定する。今回は EventEntityMigrationPolicy とした。

次に EventEntityMigrationPolicy クラスを作る。

@interface EventEntityMigrationPolicy : NSEntityMigrationPolicy {

}@end

@implementation EventEntityMigrationPolicy

- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance
entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error
{
 NSManagedObjectContext* context = [manager destinationContext];
 NSString *entityName = [mapping destinationEntityName];
 
 NSDate *timeStamp = [sInstance valueForKey:@"timeStamp"];
 
 NSCalendar* calendar = [[[NSCalendar alloc]
             initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
 NSDateComponents* components = [calendar components:NSMonthCalendarUnit|
             NSDayCalendarUnit fromDate:timeStamp];

 NSManagedObject* dInstance = [NSEntityDescription
             insertNewObjectForEntityForName:entityName inManagedObjectContext:context];

 [dInstance setValue:timeStamp forKey:@"timeStamp"];
 [dInstance setValue:[NSNumber numberWithInt:[components month]] forKey:@"month"];
 [dInstance setValue:[NSNumber numberWithInt:[components day]] forKey:@"day"];
 [dInstance setValue:@"Good morning!" forKey:@"memo"];
 
 return YES;
}
@end
sInstance にマイグレーション元のインスタンスが渡されるのでここから day と month を抽出してマイグレーション先のインスタンスへ設定する。

ポイントは次の通り。

1. 移行先のインスタンス(NSManagedObject)を生成する必要がある。
 Core Data 側でお膳立てはしてくれない。

2. すべての属性について移行の処理を実装する。
 移行処理を書かない属性は移行されない(当たり前だが)。
 (この処理はモデル情報を利用すれば定型化できる)

3. マッピングモデルで定義された属性のマッピングは参照されない。

マイグレーションプロセスは Core Data 側が管理するが、個々の具体的なマイグレーション処理は開発者が責任を持って全属性について実装する必要がある。



ソースコード


GitHub からどうぞ。
xcatsan/iOS-Sample-Code at 2010-12-06 - GitHub


備考


マイグレーションは1件毎にメソッドが呼び出されて処理される。この為、件数が多い場合は時間・メモリの面でパフォーマンスが問題になるケースが出てくる。この場合は標準のマイグレーションプロセスをカスタマイズして最適化することが可能。この方法については下記に解説がある。
Multiple Passes—Dealing With Large Datasets



参考情報


Core Data Model Versioning and Data Migration Programming Guide: Introduction to Core Data Model Versioning and Data Migration Programming Guide
公式リファレンス。基本的な情報はここでほとんど網羅されている。

Core Dataのマイグレーション(手動編) | hippos-lab::blog
NSEntityMigrationPolicy を使い単一テーブルから複数テーブルへ変換するリレーションシップを持つケースのマイグレーションの解説。わかりやすい。

CoreData - モデル、エンティティのクラス図

2010年12月5日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

現在 Core Data のマイグレーションを調査中。その為の整理としてモデルとエンティティのクラス図を Xcodeのモデルエディタで作成した。

ふむふむ。

NSManagedObjectModel .... エンティティを管理する(RDBでいうDBの定義に近い)。バージョン情報もここで扱う。
NSEntityDescription .... エンティティの定義(RDBでいうテーブルの定義)
NSPropertyDescription ... プロパティの定義(RDBでいうカラムの定義)。属性やリレーションシップの派生クラスがある。


参考情報


Core Data Programming Guide: Core Data Basics

CoreData - 大量データを扱う場合のメモリ利用量を減らす

2010年9月8日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

概要


CoreData に格納されている 6,000件のデータを CSVファイルへエクスポートする処理を走らせていたところ、どうもメモリ不足が原因で落ちてしまった。最終的にはメモリ使用量を減らすことでこの問題を回避することができた。以下はその時のInstruments のグラフ。

当初こうだったのが

こうなった。フットプリントは 1/15まで激減した。


※実機: iPhone 3GS / iOS4.0


最初のコード


下図のようなエンティティ構造を持つ CoreData のデータを CSV形式でファイルへエクスポートする。


データ件数は、Customerが 500件、Karteが6,000件(Customer1件につきKarte 12件)となっていて、それぞれを1つの CSVファイルへ書き出す。

処理は次のような感じになる。
NSMangedObjectContext* moc = [取得];
NSArray* customers = [moc Customer全件取得];

for (NSManagedObject* customer in customers) {
    NSArray* kartes = [moc Karte取得・条件:customer];
    for (NSManagedObject* karte in kartes) {
       [CSV1行書き出し];
    }
}
メモリのフットプリント(利用状況)はこんな感じ。


Customer, Karte を1件づつ読み込む度にメモリが消費され、フットプリントが増大していくのがわかる。ピーク時には 150MB程度まで膨れ上がった。

コードに手を入れてこれを改善して行こう。


改良その1 - NSAutoreleasePoolの導入


CSVエクスポート中はメインスレッドの Run Loop に制御が戻らないので、autorelease で確保したメモリの解放が行われない。そこで CSVエクスポート内で NSAutoreleasePoolインスタンスを作成するようにしてみた。NSAutoreleasePool のインスタンスを作ると、その後に autoreleaseが送られたオブジェクトは NSAutoreleasePoolの解放のタイミングで releaseが送られ解放される。
(利用例)
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]

    NSMutableDictionary* dict = [[[NSMutableDictionary alloc] init] autorelease];
    NSArray* array = [NSArray arrayWithObjects:obj1, obj2, nil];
     :
[pool release];
// このタイミングで dict, array に releaseが送られる=解放される。
NSAutoreleasePoolの局所利用は一度に大量のオブジェクトを使う場合の Cocoaプログラミングにおける定石ともいえる。早速これを組み込んでみよう。

改良後のコードはこんな感じ。
NSAutoreleasePool* pool = nil;

NSMangedObjectContext* moc = [取得];
NSUInteger numberOfCustomers = [moc Customer件数];
NSUInteger index = 0;

while (index < numberOfCustomers) {
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    NSArray* customers = [moc Customer取得 from:index count:25];
    for (NSManagedObject* customer in customers) {
        NSArray* kartes = [moc Karte取得・条件:customer];
        for (NSManagedObject* karte in kartes) {
           [CSV1行書き出し];
        }
    }
    [pool release];
    index += 25;
}
Customerの全件取得をやめて 25件毎のフェッチに切り替えてある。全件の場合 NSArray* customers 内に Customerへの参照が最後まで残ってしまうため NSAutoreleasePoolの導入が意味をなさなくなってしまう。その為、適当な件数(今回は 25件)で分割して、その度に NSAutoreleasePoolの作成と解放を行っている。

実行した結果がこれ。


メモリのフットプリントはかなり改善して1/3程度となった。増加と解放を繰り返すのでグラフはギザギザとなっている。25件毎の NSAutoreleasePool解放によってメモリ使用量が減っているのがわかる(ギザギザの谷の部分)。ただ、見ての通り全体としては右上がりでメモリの利用量は増加している。一部解放されないメモリが残っていてそれが積み上がっていく。何かが解放されていない。


改良その2 - NSManagedObjectContextのリセット


何度もコードを見直したが怪しいところが見つからない。強いて言えば NSManagedObjectContext の管理下にあるオブジェクト(NSManagedObject)が怪しい。フェッチ結果を格納している NSArrayはきちんと解放されていることはわかったので、その中に格納されているNSManagedObject にも releaseメッセージが届いているはず。解放されないメモリがあるとしたらそれはNSManagedObjectで、その原因としては NSManagedObjectContextが参照(retain)を持っているからでは? そう仮説を立てて NSManagedObjectContext内のオブジェクトを解放する方法を探したところ、以前調べたことのある -[NSManagedObjectContext reset] が見つかった。

[参考] NSManagedObjectContext Class Reference

(旧) Cocoaの日々: CoreData - トランザクション(4) reset
(旧) Cocoaの日々: CoreData - トランザクション(5) まとめ

リファレンスの説明よりの引用
All the receiver's managed objects are “forgotten.” If you use this method, you should ensure that you also discard references to any managed objects fetched using the receiver, since they will be invalid afterwards.
ずばり参照の破棄(discard references)と書いてある。これを使ってみよう。

NSAutoreleasePool* pool = nil;

NSMangedObjectContext* moc = [取得];
NSUInteger numberOfCustomers = [moc Customer件数];
NSUInteger index = 0;

while (index < numberOfCustomers) {
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
    NSArray* customers = [moc Customer取得 from:index count:25];
    for (NSManagedObject* customer in customers) {
        NSArray* kartes = [moc Karte取得・条件:customer];
        for (NSManagedObject* karte in kartes) {
           [CSV1行書き出し];
        }
    }
    [moc reset];    // NSManagedObjectContextをリセット
    [pool release];
    index += 25;
}

するとこうなった。

減った。

NSAutoreleasePool だけでは解放しきれなかったメモリのフットプリントが綺麗になくなっている。その結果 25件毎にメモリ利用量は元に戻り、最大でも 10MB程度しかメモリを使わなくなった。やはり NSManagedObjectContextが参照しているオブジェクト(NSManagedObjectが主)が原因だったようだ。これは強力だ。

実機で試したところ 6,000件の CSVファイル書き出しを行っても落ちなくなった。


補足


今回のアプリの場合、CSVエクスポートは通常使う機能とは独立した機能として実装されている。この為、NSManagedObjectContext をリセットしても他の機能への影響はなかった。しかし既に直前の画面でフェッチを行い多数の NSManagedObjectが存在する状態であったり、またはトランザクションの最中でこの resetを呼び出すと、それまでの NSManagedObjectがすべて解放され、トランザクションはすべて rollbackされてしまう。この為、-[NSManagedObjectContext reset]は今回のケースにおいては非常に有用だったが利用できる場面は限られる。

10/7追記
なんのことはない NSManagedObjectContext を重い処理専用に作れば他に気兼ねなく自由に resetできる。「利用出来る場面は限られる」なんて書いてしまったが嘘でした...
※NSManagedObjectContextは複数作れます。
[参考] (旧) Cocoaの日々: Core Data : 複数の NSManagedObjectContext を使う - Optimistic Locking


Instruments


SDK付属のパフォーマンスチューニング用の各種測定ツール。

Xcodeからは「実行」メニューの「パフォーマンスツールを使って実行」から利用することができる。今回は "Allocations"測定に使った。


他にメモリリーク(Leaks)などを調べることもできる。


参考情報


一時オブジェクトを大量に使う(メモリを消費する)処理をループする場合は、ループの中で自動解放プールを生成する - 24/7 twenty-four seven


- - - - - -
パフォーマンスが劇的に改善できた時ほど気分がいいものはないw

CoreData - awakeFromSnapshotEvents:

2010年8月8日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

NSManagedObjectContextに対して rollback を送ると、対象となる NSManagedObject の awakeFromSnapshotEvents: が呼び出される。

NSManagedObject Class Reference - awakeFromSnapshotEvents:

引数には実行された操作の種類が渡される。
enum {
   NSSnapshotEventUndoInsertion = 1 << 1,
   NSSnapshotEventUndoDeletion = 1 << 2,
   NSSnapshotEventUndoUpdate = 1 << 3,
   NSSnapshotEventRollback = 1 << 4,
   NSSnapshotEventRefresh = 1 << 5,
   NSSnapshotEventMergePolicy = 1 << 6
};
typedef NSUInteger NSSnapshotEventType;

このメソッドは操作が行われた後に呼び出される。

- - - -
rollback直前のイベントを取るにはどうしたらいいのか。

CoreData - 最大値をもつ NSManagedObject を取得するコード見本

2010年8月2日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

以前、最大値の求め方について書いた。

Cocoaの日々: Core Data - 最大値を取得する

では、その最大値を持つ NSManagedObject を取得するにはどうしたら良いのか?


コード例


こんな感じ。
- (NSManagedObject*)lastTimeStampObject
{
    NSManagedObjectContext* moc = self.managedObjectContext;
    NSFetchRequest* request = [[[NSFetchRequest alloc] init] autorelease];

    NSEntityDescription* entity = [NSEntityDescription entityForName:@"Event"
                                              inManagedObjectContext:moc];
    [request setEntity:entity]; 

    NSSortDescriptor* sort = [NSSortDescriptor sortDescriptorWithKey:@"timeStamp"
                                                           ascending:NO];
    [request setSortDescriptors:[NSArray arrayWithObject:sort]];
    [request setFetchLimit:1];

    NSError* error = nil;
    NSArray* results = [moc executeFetchRequest:request error:&error];
    
    if ([results count] > 0) {
        return [results objectAtIndex:0];
    } else {
        return nil;
    }
}

timeStamp の降順で検索をかけ、1番目の要素を返している。
出力されているSQLは次の通り。
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZTIMESTAMP
 FROM ZEVENT t0 ORDER BY t0.ZTIMESTAMP DESC LIMIT 1

-[NSFetchRequest setFetchLimit:1] を指定により SQLでも 'LIMIT 1'が付与されている。この為結果は1もしくは0(まったくレコードが無い)のどちらかになる。



サンプル


Xcodeで生成される CoreDataサンプルコードに手を加えてみた。


左下のボタンを押すと最大値を持つ NSManagedObject の descriptionがデバッグコンソールへ出力される。

ソースコード


GitHub からどうぞ
LargestManagedObject at 2010-08-02 from xcatsan's iOS-Sample-Code - GitHub

CoreData - 集計関数

2010年8月1日日曜日 | Published in | 1 コメント

このエントリーをはてなブックマークに追加

集計指示子


以前、最大値を求めるコードを紹介した。
Cocoaの日々: Core Data - 最大値を取得する

この時は -[NSExpression expressionForFunction:argument:] の第一引数に @"max:" を渡していた。
NSExpression *expression =
 [NSExpression expressionForFunction:@"max:"
   arguments:[NSArray arrayWithObject:keyPathExpression]];
ここを sum: にすると合計になるし count: だと個数を求めることができる。Objective-C コードは長いものになるが、実際に発行されている SQL はたった一行になる。以下、max:, sum:, count: を指定した場合の実例。
CoreData: sql: SELECT max( t0.ZTREATEDDATE) FROM ZKARTE t0 WHERE  t0.ZCUSTOMER = ? 
CoreData: sql: SELECT COUNT( t0.ZTREATEDDATE) FROM ZKARTE t0 WHERE  t0.ZCUSTOMER = ?
CoreData: sql: SELECT total( t0.ZFEE) FROM ZKARTE t0 WHERE  t0.ZCUSTOMER = ? 


関数


あらかじめ利用可能な関数が定義されている。定義は下記のリファレンスで確認できる。
NSExpression Class Reference - expressionForFunction:argument

以下に引用する。
average:
sum:
count:
min:
max:
median:
mode:
stddev:
add:to:
from:subtract:
multiply:by:
divide:by:
modulus:by:
sqrt:
log:
ln:
raise:toPower:
exp:
ceiling:
abs:
trunc:
random
random:
now
floor:
uppercase:
lowercase:
bitwiseAnd:with:
bitwiseOr:with:
bitwiseXor:with:
leftshift:by:
rightshift:by:
onesComplement:
noindex:

標準的な集計関数の他、random や now といったユニークなものまで用意されている。

CoreData - setFetchLimit:

2010年7月29日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

-[NSFetchRequest setFetchLimit:1] とすると1件だけ結果を取り出すことができる。

NSFetchRequest Class Reference

0 指定の場合は無制限となる。


発行されている SQLを見ると LIMIT が適用されているのがわかる。
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZTIMESTAMP
 FROM ZEVENT t0 ORDER BY t0.ZTIMESTAMP DESC LIMIT 1


類似の設定として -[NSFetchRequest setFetchBatchSize:] がある。こちらは1度に取得する件数を指定する(例:5を指定した場合、10件のデータがある場合は2回SQLが飛ぶ)。

[参考情報] (旧) Cocoaの日々: CoreData - SQLite の LIMIT

NSFetchedResultsControllerDelegate - メモリ管理に関するメモ

2010年7月28日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

NSFetchedResultsController を使っていて、NSFetchedResultsControllerDelegate を実装している時のメモリ管理に関する私的メモ。


Delegateのメソッドはいつ呼ばれるのか?


- controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: は、NSManagedObjectContext に変化があった時に呼ばれる。

次のケースを想定してみる。

UINavigationController を使っていて、一覧画面から詳細画面へ移動してそこで NSManagedObjectContextに操作を加える。
ListViewController &ltNSFetchedResultsControllerDelegate> ⇒ (参照) UITableView* tableView
 ↓
DetailViewController ← NSManagedObjectContext操作(変更・削除など)
すると ListViewController の -controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: が呼び出される。この時点ではまだ DetailViewController が表示されているものとする。

この Delegateメソッド内では通常 tableViewに対して操作を行っている。
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath {

    UITableView *tableView = self.tableView;

    switch(type) {
            
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
               withRowAnimation:UITableViewRowAnimationFade];
            break;
         :

上記のケースでメモリ不足が発生して ListViewController の viewが 開放された場合、self.tableView へのアクセスが安全かどうかが気になる。


ListViewController.view が UITableView の場合


通常 ListViewController は UITableViewController のサブクラスとなる。UITableViewの管理は親クラスの tableViewインスタンスで管理される。

このケースは問題ない。

流れとしては次のようになる。
DetailViewController表示
 ↓
メモリ不足発生
 ↓
ListViewController で viewDidUnload が呼び出され、view(tableView) が開放される
 ↓
DetailViewController で NSManagedObjectContext を操作
 ↓
ListViewController で controllerWillChangeContent: が呼び出される。
この中で self.tableView を参照。
   ↓
  self.tableViewへのアクセスをトリガーにして、ListViewController で
  viewDidLoad が呼び出される。これによって self.tableView の準備が完了する
   ↓
  controllerWillChangeContent: の処理を実行
 ↓
ListViewController の -controller:didChangeObject:atIndexPath:forChangeType:
 newIndexPath:呼び出しself.tableView への操作が無事に行われる
 ↓
ListViewController へ戻ると、変更が反映された表が表示されている。

UITableViewController を使う場合、self.view と self.tableView は等価となる。このため self.tableViewに対するアクセスによって、self.view に仕掛けられた KVO機構が発動して UITableViewがロードされ self.tableView で使えるようになると思われる(※これは推測)。=> KVOなんて使わなくて単なる getterメソッドの実装でそうなっている、と思われる。


ListViewController.view が UIView の場合


UIView の上に UITableView が載っているケース。この場合、ListViewController は UIViewController のサブクラスで tableView を定義して自前で管理する。
@interface RootViewController : UIViewController
  <NSFetchedResultsControllerDelegate> {

    UITableView* tableView_;
}
@property (nonatomic, retain) IBOutlet UITableView* tableView;
@end

@implementation RootViewController
  :
- (void)viewDidUnload {
    [super viewDidUnload];
    self.tableView = nil;
}
  :

結論からすると、このケースも実質問題がない。

DetailViewController表示
 ↓
メモリ不足発生
 ↓
ListViewController で viewDidUnload が呼び出され、view が開放される
 ↓         この時 tableView も開放される(self.tableView=nil)
 ↓
DetailViewController で NSManagedObjectContext を操作
 ↓
ListViewController の
-controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:呼び出し
 ↓
self.tableViewに対してメッセージを送るが nil の為、何も起こらない
 ↓
ListViewControllerへ戻る
 ↓
viewが再ロードされ viewDidLoad が呼び出される。
UIViewControllerが参照するビューが芋づる式にロードされる。
 ↓
UITableView は新規表示になるのでデータは最新のものを読み直し。
この結果、正しいデータが表示される。


ソースコード


両方のケースを試せる。ただし切り替えは手作業が少々必要(現在は1番目のケースで動作する)。
NSFetchedResultControllerDelegateSample at 2010-07-28x from xcatsan's iOS-Sample-Code - GitHub


関連情報


Cocoaの日々: NSFetchedResultsControllerDelegate を使う

NSFetchedResultsControllerDelegate を使う

2010年7月25日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: NSFetchedResultsController のおさらい

今回は NSFetchedResultsControllerDelegate について調べた。


NSFetchedResultsControllerDelegate


NSFetchedResultsControllerDelegate Protocol Reference

NSFetchedResultsControllerDelegate は NSFetchedResultsController からのコールバックを受け取るためのメソッドが定義されているプロトコル。NSFetchedResutlsController は NSManagedObjectContext に対する操作(追加・変更・削除)を監視していて、それらを検出するとこのプロトコルのメソッドを呼び出す。この辺りは前回の Cocoaの日々: NSFetchedResultsController のおさらい にて少し触れた。

これらのメソッドの使い方は上記リファレンスの Overviewに書かれている。また Xcodeで Core Data を使うプロジェクトを作成すると、これらの実装コードが自動的に生成される。


利用パターン


利用パターンは3つ。

[A] 操作毎に処理するパターン


このパターンは NSManagedObjectContext の変更毎に呼び出されるメソッドを実装し、処理を行うパターン。Overviewではこれらが "Typical Use"として紹介されている。Xcodeが生成するコードもこのパターンになっている。

具体的には次のメソッドを実装する。
– controllerWillChangeContent:
– controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
– controller:didChangeSection:atIndex:forChangeType:
– controllerDidChangeContent:

通常はこれらの処理で UITableView に対する操作を行う。以下、controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: の実装例の引用:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
    atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
    newIndexPath:(NSIndexPath *)newIndexPath {
 
    UITableView *tableView = self.tableView;
 
    switch(type) {
 
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            break;
 
        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            break;
 
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath]
                  atIndexPath:indexPath];
            break;
 
        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}

操作毎に UITableView のアニメーション付きで表示更新を行うと、ユーザには一件づつ処理が行われているのが視覚的にわかるようになる。


[B] 操作終了時のみ処理するパターン


controllerDidChangeContent: のみ実装するパターン。これは処理対象の件数が多く [A]のパターンではパフォーマンス的に問題が出る場合に採用する。例えば100件のデータを削除するなど。1件づつ視覚的フィードバックを行うと非常に時間がかかるため現実的ではない。この場合は操作最後に UITableView の全件読み直しを行う方法が取れる。

[実装例]
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView reloadData];
}


[C] デリゲートを利用しないパターン


NSFetchedResultsController.delegate = nil とするパターン。この場合は UITableView と NSManagedObjectContextとの同期を自前で処理する。


- - - -
通常は [A] を使い、処理件数が多い場合のみ [B] を使うといったハイブリッドなパターンも考えられる。


サンプルプログラム


動作確認するために複数の行を選択・削除できるサンプルプログラムを作ってみた。

[ソース] FetchedResultsControllerSample at 2010-07-25 from xcatsan's iOS-Sample-Code - GitHub



行を複数選択して、右下のボタンを押すと選択された行がアニメーションしながら表から消える。

サンプルコードのベースは、Xcodeの "Navigation-based Application" + "Use Core Data for storage" を使った。モデルの定義と調整、行の複数選択以外は手を入れていない。NSFetchedResultsControllerDelegate の実装メソッドは Xcodeが生成した雛形をそのまま利用した。つまり [A]パターンの動作となる。これだけで最低限の処理ができるので便利だ。


なお試しに NSFetchedResultsControllerDelegate の実装メソッドを controllerDidChangeContent: だけにしてみた([B]パターン)。この場合は [A]パターンと違いアニメーションは起こらず、画面全体が読み直される。


参考情報

Table View Programming Guide for iOS: Inserting and Deleting Rows and Sections
"Batch Insertion, Deletion, and Reloading of Rows and Sections" UITableViewのバッチ操作についての解説。

人気の投稿(過去 30日間)