Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iPhone app launch times and Core Data migration

I have a Core Data application which I plan to update with a new schema. The lightweight migration seems to work, but it takes time proportional to the amount of data in the database. This occurs in the didFinishLaunchingWithOptions phase of the app.

I want to avoid <app> failed to launch in time problems, so I assume I cannot keep the migration in the didFinishLaunchingWithOptions method.

I assume the best method would involve performing the migration in a background thread. I assume also that I'd need to defer loading of the main ViewController until the loading completes to avoid using the managedObjectContext until initialization completes.

Does this make sense, and is there example code (maybe in Apple sample projects) of this sort of initialization?

like image 212
sehugg Avatar asked May 18 '10 19:05

sehugg


3 Answers

You can't put migration in a NSOperation because it will need to run on the main thread. What you need to do is to get out of the -applicationDidFinishLaunching: method without touching the Core Data stack. If you can finish that method (and that run loop cycle) quickly then your app will not be terminated and you can take as long as the user will put up with to finish your migration.

See my answer here: How to switch from Core Data automatic lightweight migration to manual?

Update May 19, 2010

To clarify my position on this. It is inherently possibly to do just about anything. However, doing a migration on a background thread is a bad idea. It is extremely difficult to guarantee that the stack will never get touched during the migration as well as a whole host of other threading specific complications.

It is possible to do it but it involves a high degree of risk that is completely unnecessary. The main thread can and should be used to do a migration of the main Core Data stack. It is easy to put up a modal dialog to let the user know a migration is occurring and then perform the migration on the main thread.

If you are in a situation where your migrations are taking a signficant amount of time then it is also highly recommended that you switch from automatic migration to manual migration with a mapping model so that you can:

  • Easily back out of the migration if needed.
  • Perform the migration in chunks in case the user quits your application.
  • Give the user solid feedback on how far along the migration is and when it will be done.

Update December 15, 2015

A lot has changed since this was originally answered.

The answer now for migrations is to fire them on a background queue (via dispatch_async of the NSPersistentStoreCoordinator's addStore... call).

This also means that you need to make sure your UI can handle the persistence layer being empty/not available for an unknown period of time. How to do that depends on your application.

For example, you can have a temporary UI that shows dancing cats while the persistence layer does the migration. The user experience is up to you.

However, you do NOT want to let the user create data while the migration is happening. That will make it difficult to merge later (if at all).

like image 74
Marcus S. Zarra Avatar answered Nov 03 '22 17:11

Marcus S. Zarra


Sorry Marcus, I have to respectfully disagree. You can migrate in the background.

My migriation runs on a background thread. It can take over 10 seconds on slow devices, so I start it on a background thread and have a specific modal view controller to show progress.

The way to do this is split your normal loading sequence into two phases.

Phase 1) Do everything you would normally do at launch that doesn't require managed objects. The end of this phase is defined by a check to determine if migration is required.

Phase 2) Do everything that normally happens at launch that requires managed objects. This phase is an immediate continuation of Phase 1) when no migration was required.

This way, your app finishes launching regardless of the duration of migration processing.

To perform a long migration successfully, I use a modal view controller showing feedback of the migration progress to the user. I then commence the migration in the background thread, while the modal view controller uses KVO to update it's progress bar.

At the end of the migration, it closes down the whole core data "stack", and a callback to the main thread will dismiss the modal and continue on to Phase 2).

This whole process works flawlessly, although I still have an open question on a way to have the automatic lightweight migration reveal it's progress like the manual migration does.

like image 20
ohhorob Avatar answered Nov 03 '22 17:11

ohhorob


Here is an outline of an approach similar to what ohhorob describes. The difference is that I just display an animated "Upgrading..." HUD rather than a more informative progress bar.

The key point is that nothing tries to access Core Data until the migration has had a chance to do its thing. Therefore, I move all the remaining launch code (including the setting of the rootViewController) that could potentially touch Core Data into a separate method. I then call this method after the migration has completed, or immediately if no migration is required. That looks like this:

-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    [self.window makeKeyAndVisible];

    if ([MyManagedObject doesRequireMigration]) { // -doesRequireMigration is a method in my RHManagedObject Core Data framework
        [SVProgressHUD showWithStatus:NSLocalizedString(@"Upgrading...", nil) maskType:SVProgressHUDMaskTypeGradient];

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

            // do your migration here

            dispatch_async(dispatch_get_main_queue(), ^{
                [SVProgressHUD showSuccessWithStatus:NSLocalizedString(@"Success!",nil)];
                [self postApplication:application didFinishLaunchingWithOptions:launchOptions];
            });
        });

    } else {
        [self postApplication:application didFinishLaunchingWithOptions:launchOptions];
    }

    return YES;
}

-(void)postApplication:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window.rootViewController = ....

    // anything else you want to run at launch
}

This approach should play nice with the watchdog that monitors startup times since the migration will take place off the main thread.

like image 2
chris Avatar answered Nov 03 '22 19:11

chris