Do you exactly initialize the Core Data stack correctly?



Have you ever noticed that any of your favorite iOS apps stopped working after the next update, or did it start for half a minute? Usually after that, their developers release an urgent bugfix. And this is not always associated with bugs in the code of the final programmer, sometimes the problem lies a little deeper.

It seems rather strange to me that this error occurs quite often (and should occur in "serious" projects), but for some reason they are silent about it.
This article will discuss a standard error when initializing the CoreData stack in iOS applications.

The CoreData framework is a powerful tool in the hands of Cocoa developers, almost free persistence, ease of data change, recording, version support, migration from model to model - all that is so often necessary in our projects. Someone uses it in readonly mode, someone saves quite a bit and works with such a small selection, but someone uses it to the fullest.

I have to say right away that in order to encounter an error, there are three reasons to collect: a large amount of the database, initialization of the CoreData stack in the main thread, changes to the database schema in the new version of your program. Let's look at how easily all of them can be assembled as a developer.


Reason # 1. Large database file size

When used "to the full" it often happens that the size of the base is artificially unlimited and can easily occupy a gigabyte.

For examples of the latter, you do not need to go for a long time. On the Internet, people are still wondering if pictures can be stored in a database (by the way, in this article, CoreData and the database will be almost synonymous). Quite popular answers that have the approval of the Stackoverflow audience suggest that you can safely store them in a database up to a picture up to a megabyte. For example, here stackoverflow.com/questions/2573072/coredata-store- images-to-db-or-not The
answer looks like this:

< 100kb store in the same table as the relevant data
< 1mb store in a separate table attached via a relationship to avoid loading unnecessarily
<1mb store in a separate table attached via a relationship to avoid loading unnecessarily
> 1mb store on disk and reference it inside of Core Data
The reasons for the large volumes figured out.

Reason number 2. Initialization in the main thread

Well, can there really be doubts? I think that about 100% of newcomers, and certainly about 70% of developers, will more fully initialize the entire CoreData stack in the main program flow.

Reason number 3. Need to migrate to a new data schema

Usually, when changing the database schema (model), CoreData transfers data from the old database to the new one if you set the appropriate rules. The simplest so-called lightweight migration is easy, you need to pass the options dictionary when connecting the storage to NSPersistentStoreCoordinator:

NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, 
                                               [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
[_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:optionsDictionary error:&error];



If the transfer does not fit into the framework of lightweight migration, then the developers will implement their custom, but the essence of reason No. 3 does not change.

Reasons collected


The reasons are compiled. Pretty simple to assemble them, isn't it? Well, just these reasons are simply collected by many developers who once inserted the initialization code at the dawn of the formation of their project and safely forgot about it until they saw something like this on iTunes:

Вылетает на старте, перед этим долго тупит!

203 из 203 покупателей считают эту рецензию полезной


What's happening?


I think all these reasons lead you to a simple conclusion. And he is faithful! Even simply copying migration data to a new version of the database takes time, and the longer the database file takes, the longer it takes. And we are talking about simple copying, and if you need to reindex fields, for example?

The bottleneck has already been mentioned - this is the connection of persistent storage to the NSPersistentStoreCoordinator object. This is where the reasons put together create a problem. And if your application does not respond for 30 seconds, then the system closes it.

Decision


Fortunately for us, you can create an NSPersistentStoreCoordinator and connect storage to it in another thread. And at the time of the data initialization, it’s a good tone to show some window with the inscription “Data update”, for example.

Here's how it will look in the code (I’ll say right away that the solution does not claim to be perfect, if someone thinks better, write in the comments).


// Выносим весь код инициализации GUI из этого метода
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.launchOptions = launchOptions;
    
    [self performSelectorInBackground:@selector(initCoreData) withObject:nil];
    [NSThread sleepForTimeInterval:0.2]; // время, в течение которого может пройти подключение хранилище, если никаких изменений нет. Дешевое средство избегания ненужных миганий на экране
    [self initialDisplayGUI];
    
    return YES;
}

- (void)initialDisplayGUI {
    if (self.dataIsReady) {
        [self diplayAllGUIStuff];
    } else {
        self.dataPrepareController = [[MigrationProgressViewController alloc] init];
        [dataPrepareController setDoneTarget:self withAction:@selector(diplayAllGUIStuff)];
        dataPrepareController.view.frame = window.frame;
        [window addSubview:dataPrepareController.view];
        [window makeKeyAndVisible];
    }
}

- (void)initCoreData {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    if (self.persistentStoreCoordinator) {
    	NSLog(@"Storage was added");
    }
    self.dataIsReady = YES;
    [pool release];
}

- (void)setDataIsReady:(BOOL)dataIsReady {
	if (_dataIsReady != dataIsReady) {
		_dataIsReady = dataIsReady;

		[self performSelectorOnMainThread:@selector(diplayAllGUIStuff) withObject:nil waitUntilDone:NO];
	}
}



-diplayAllGUIStuff a method that contains the code that you had in - (BOOL) application: didFinishLaunchingWithOptions:
The MigrationProgressViewController is needed to display, for example, indicators of the remaining time or at least show that the process has not hung. Its only task is to reassure the user. It’s more pleasant for users to look even at the “bare” UIActivityIndicatorView, than at the dangling screen saver, and even more so having a crashed application on hand.

That’s probably all. Avoid such a coincidence and often review the code, especially the one that Xcode inserted for you.
image