11

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?

sehugg
  • 3,615
  • 5
  • 43
  • 60

5 Answers5

14

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).

Community
  • 1
  • 1
Marcus S. Zarra
  • 46,571
  • 9
  • 101
  • 182
  • I'm wondering if this is still your view on current best practice (circa 2015)? Thanks! – Benjohn Dec 15 '15 at 19:07
  • 1
    No, I would put the migration on a background queue and let it finish before letting the UI finish. My more recent discussions on the Core Data stack reflect that change. – Marcus S. Zarra Dec 15 '15 at 21:59
7

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.

Community
  • 1
  • 1
ohhorob
  • 11,695
  • 7
  • 41
  • 50
  • This sounds like what I need, except I would have to defer the loading of `NSMainNibFile` by `UIApplication` since all of my views touch Core Data. I'm sure there's a way to do this manually but I haven't found the right reference. – sehugg May 19 '10 at 17:37
  • Absolutely. I would remove the `NSMainNibFile` from the info plist, and load it when you know everything is migrated and ready to go. I remember reading this http://www.dragthing.com/blog/?p=246 by James Thomson (developer of PCalc) doing this manually to assist with his perceived launch times. – ohhorob May 19 '10 at 18:12
  • Thanks for the link .. that's what I figured just scared at getting the exact method right ;) If migration is required, I `loadNibNamed` a loading screen and add it to the window, then `makeKeyAndVisible`. Then I `performSelector` with delay of 0.0 to Part II of the loading process, which tweaks the run loop. Then I load the main view .nib which adds it to the window automatically (i'm not clear on how exactly that works). – sehugg May 19 '10 at 19:12
  • 6
    I would put that at a expert level design. Trying to do that on a background thread is extremely dangerous and very easy to screw up. For those reading this as a solution, I do not recommend it **AT ALL**. You are far better off putting up the exact same modal view and then processing the migration on the main thread. Far fewer chances of screwing up the user's data. Hence my blanket recommendation of doing all migrations on the main thread. – Marcus S. Zarra May 19 '10 at 23:16
  • So what was once something you can't do is now a recommendation. – Alex Reynolds May 19 '10 at 23:34
  • Please read what I said. I said that I recommend doing migrations on the main thread only. I prefer to use the word can't as it gets heard a lot better than "you really really really should not do this". – Marcus S. Zarra May 19 '10 at 23:46
  • 1
    I agree with Marcus on this. It's far safer to use manual migration than trying to hack the automatic migration to run on a background thread. I've learned to be very cautious around multithreading and Core Data. – Brad Larson May 20 '10 at 02:37
2

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.

chris
  • 16,324
  • 9
  • 37
  • 40
0

I just found a pretty simple approach. Just wrap all your calls in application:didFinishLaunching with a dispatch_async to the main thread. This will return right away and let you perform your upgrade on the main thread. However, you should probably set your window so that it doesn't display a blank screen while migrating.

- (void)application:didFinishLaunching {
     dispatch_async(main_queue) {
          // migration
     }
     self.window = ...
     return YES;
}
0

You might put your Core Data updates into an NSOperation, which can be added to an operations queue in didFinishLaunching... and which can operate in the background, by overriding the operation's -main method.

Take a look at this tutorial page to get a general idea of what's involved. Use KVO with the operation's isFinished property to update the state of your application -- you might use this key's value to warn the user that the migration is still taking place, for example, before presenting any data.

Alex Reynolds
  • 95,983
  • 54
  • 240
  • 345
  • http://stackoverflow.com/questions/2840406/nsoperations-or-nsthread-for-bursts-of-smaller-tasks-that-continuously-cancel-eac maybe helpful in some way. – RickiG May 18 '10 at 19:32
  • You can't put the migration in a `NSOperation` because A) it needs to be the main context and a `NSOperation` would put it in the background; and B) any code that touches the `NSManagedObjectContext` on the main thread would trigger a *second* migration while the background thread is running. – Marcus S. Zarra May 19 '10 at 15:05
  • I do background Core Data work with `NSOperation` instances, so I think I have to respectfully disagree. I read through the migration guide again and there is nothing in there about this needing to be done on the main thread. – Alex Reynolds May 19 '10 at 17:10
  • Any advantage in using NSOperation as opposed to blocking the main run loop, if your app can't do anything without Core Data instantiated anyway? – sehugg May 19 '10 at 19:15
  • Your UI can operate unimpeded on the main thread. So your app can provide UI cues to the user that a migration is still in progress, for example. – Alex Reynolds May 19 '10 at 19:19