iPhone app launch times and Core Data migration
Asked Answered
D

5

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?

Degauss answered 18/5, 2010 at 19:12 Comment(0)
H
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).

Hemmer answered 19/5, 2010 at 15:4 Comment(2)
I'm wondering if this is still your view on current best practice (circa 2015)? Thanks!Periodate
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.Hemmer
H
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.

Helgoland answered 19/5, 2010 at 17:6 Comment(7)
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.Degauss
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 dragthing.com/blog/?p=246 by James Thomson (developer of PCalc) doing this manually to assist with his perceived launch times.Helgoland
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).Degauss
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.Hemmer
So what was once something you can't do is now a recommendation.Pirtle
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".Hemmer
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.Corydalis
B
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.

Burgeon answered 9/4, 2013 at 12:52 Comment(0)
P
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.

Pirtle answered 18/5, 2010 at 19:28 Comment(5)
#2840906 maybe helpful in some way.Joyce
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.Hemmer
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.Pirtle
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?Degauss
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.Pirtle
S
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;
}
Stoeber answered 24/4, 2013 at 3:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.