[CoreData]起動時間が長いとアプリが強制終了する

2011/07/24

AppStoreでアプリがアップデート公開されたら、とんでもない自体になりました。

バージョンアップしたらアプリが起動しない!

と恐ろしい報告が。。。

今回のアップデートはSQLite に 12 カラム追加したので恐らく CoreData あたりかな?と思うのですが、migrate の仕方も問題ないですし開発でもアップデートできているので・・・原因が掴めません。

アプリのログを確認する

まず、アプリのログを見たい!ってことで、どうすればいいのかと言うと、 (1) iPhone のアプリを iTunes 同期させる。 CoreData (2) 以下のディレクトリにクラッシュログが保存されている。

/Users/ユーザ/Library/Logs/App Store/CrashReporter

するとこんなログが

execution code が 0x8badf00d ("ate bad food")

これを元にググってみる。

CoreData のマイグレーションは時間がかかる

CoreData - マイグレーションを考慮した CoreDataManager パターン の記事で解決しました。 (まだAppStoreで公開されていないので、多分ですが・・・) CoreData に限らず

application:didFinishLaunchingWithOptions で制限時間を超えると起動しない

のです。 改めて前のバージョンの開発バージョンに戻して試した結果

データ200件超で、カラム12個追加で 30秒くらいでした

ということで、自分のアプリの対策をすることにしました。

マイグレーション判別の画面を追加

一番の肝は、didFinishLaunchingWithOptionsで マイグレーションの待機処理させない事です。 問題のアプリの構成は、

(1) UITabView で複数画面存在する (2) 最初の起動画面で CoreData にアクセスしての一覧表示 (3) 他の画面でも CoreData にアクセスする

シーケンス的には、

didFinishLaunchingWithOptions 起動 → UITabView 起動 → ViewController 起動 → マイグレーション判別と処理 ※非同期処理 → トップ画面表示

重要なのは、dispatch_async(GCD) のブロック文で非同期処理する事です。 でないと、didFinishLaunchingWithOptions で待機してしまい、また時間制限で終了してしまいます。

ということで、まずUITabView にマイグレーション処理する ViewController を追加。 CoreData

CoreData

ユーザにあまり違和感を与えないように、Top画面としました。 この辺は作るアプリによって違いますので、それぞれ考える必要がありますね。

マイグレーションを判別する

CoreData - マイグレーションを考慮した CoreDataManager パターン の記事では、CoreDataManager を別途作ってやっていますが、それをすると今回は修正が大変なので AppDelegate 経由で処理させることに。 ただし、あくまでも AppDelegate に 一般的な CoreData の処理が実装されている話が前提です。

まずは、マイグレーションを判別するコード

- (BOOL)isRequiredMigration {
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Sample.sqlite"];
    NSError* error = nil;
    
    NSDictionary* sourceMetaData = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
                                                               URL:storeURL
                                                             error:&error];
    if (sourceMetaData == nil) {
        return NO;
    } else if (error) {
        NSLog(@"Checking migration was failed (%@, %@)", error, [error userInfo]);
        abort();
    }
    
    BOOL isCompatible = [self.managedObjectModel isConfiguration:nil
                                        compatibleWithStoreMetadata:sourceMetaData]; 
    
    return !isCompatible;
}

isConfiguration:nil

マイグレーションの更新

マイグレーションの更新に関しては、従来通り persistentStoreCoordinator で処理すればいいけど、気分的に明示的に別途コードを追加。

- (NSPersistentStoreCoordinator *)doMigration {
    NSLog(@"--- doMigration ---");
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Sample.sqlite"];
    
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
                             nil];
    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])
    {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }    
    
    return __persistentStoreCoordinator;
}

マイグレション更新という意味では、NSPersistentStoreCoordinator を返す必要はないので void か BOOL でよいかも。

マイグレーション実行画面を作る

UITabView で最初に起動される TopViewController とマイグレーション実行画面 MigrateViewController も作成。 CoreData マイグレーション TopViewController の ViewDidLoad にマイグレーション更新時に TopViewController を起動させる処理を記述。

- (void)viewDidLoad {
    [super viewDidLoad];
    SampleAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];

    if ([appDelegate isRequiredMigration]) {
        [self showMigrateView];
    }
}

- (void)showMigrateView {
    SampleAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    MigrateViewController *controller = [[MigrateViewController alloc]
                                                    initWithNibName:@"MigrateViewController"
                                                    bundle:nil];
    
    controller.delegate = self;
    controller.view.alpha = 0.0f;
    [appDelegate.window addSubview:controller.view];
    
    [UIView animateWithDuration:0.5f
                          delay:0.0f
                        options:UIViewAnimationOptionAllowUserInteraction
                     animations:^{
                         [controller.view setFrame:CGRectMake(0, 0, 320, 480)];
                         [controller.view setAlpha:1.0f];
                     }
                     completion:^(BOOL finished){
                         
                     }];
    
}

最後に、MigrateViewController が起動された時にマイグレーション更新処理をする。

- (void)doMigration {
        SampleAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    [appDelegate doMigration];
}

- (void)viewWillAppear:(BOOL)animated {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    self.message.text = NSLocalizedString(@"MIGRATION_MESSAGE_UPDATE", nil);
    self.okButton.hidden = YES;
    self.indicator.hidden = NO;
    
    dispatch_async(queue, ^{[self doMigration];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.message.text = NSLocalizedString(@"MIGRATION_MESSAGE_RESULT", nil);
            self.indicator.hidden = YES;
            self.okButton.hidden = NO;
        });
    });
    
}

これで、CoreData のマイグレーションが発生した場合は、非同期で更新処理がされるはずです。 いやぁ、本当に盲点でした。過信はいけませんねぇ。