А вы точно инициализируете CoreData-стек правильно?

в 10:33, , рубрики: coredata, db migration, ios development, sqlite, Блог компании Luxoft, разработка под iOS, метки: , , ,

А вы точно инициализируете CoreData стек правильно?

Вы когда-нибудь замечали, что какое-нибудь из ваших любимых приложений для iOS после очередного обновления перестало работать или же запуск его затягивается на полминуты? Обычно после этого их разработчики выпускают срочный багфикс. И это не всегда связано с багами в коде конечного программиста, иногда проблема лежит чуть глубже.

Мне кажется довольно странным, что эта ошибка возникает довольно часто (и должна возникать в «серьезных» проектах), но о ней почему-то умалчивают.
В этой статье речь пойдет о стандартной ошибке при инициализации CoreData-стека в iOS-приложениях.

Фреймворк CoreData — это мощное средство в руках Cocoa–разработчиков, почти бесплатная персистентность, легкость изменения данных, записи, поддержка версий, миграция с модели на модель — все то, что так часто необходимо в наших проектах. Кто-то использует его в readonly mode, кто-то сохраняет совсем чуть-чуть и работает с такой маленькой выборкой, ну а кто-то же использует его на полную катушку.

Скажу сразу, для того, чтобы встретиться с ошибкой, нужно собрать три причины: большой объем базы, инициализация стека CoreData в главной потоке, изменения схемы базы данных в новой версии вашей программы. Давайте рассмотрим, как легко все их можно собрать как разработчику.
А вы точно инициализируете CoreData стек правильно?

Причина №1. Большой размер файла базы

При использовании «на полную» часто бывает так, что размер базы искусственно ничем не ограничен и может легко занимать и гигабайт.

За примерами последних долго ходить не надо. В интернете до сих пор люди задаются вопросом, можно ли хранить картинки в базе данных (кстати, в этой статье CoreData и база данных будут почти синонимами). Довольно популярные ответы, имеющие одобрение Stackoverflow-аудитории говорят о том, что до картинки до мегабайта хранить можно смело в базе данных. Например, тут stackoverflow.com/questions/2573072/coredata-store-images-to-db-or-not

Ответ выглядит так:

< 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 on disk and reference it inside of Core Data

Или же ваше приложение может хранить вашу активную переписку пользователя за последние три года (мессенджер, почтовый клиент и пр).
С причинами больших объемов разобрались.

Причина №2. Инициализация в главном потоке

Ну тут разве могут быть сомнения? Думаю, процентов 100% новичков и точно процентов 70% разработчиков по-опытнее инициализируют весь стек CoreData в главном потоке выполнения программы.

Причина №3. Необходимость миграции на новую схему данных

Обычно при изменеии схемы базы данных (модели) СoreData переносит данные из старой базы в новую, если вы задали соответствующие правила. Самая простая т.н. легковесная миграция делается просто, вам нужно передать словарь опций при подключении хранилища к 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];

Если перенос не укладывается в рамки легковесной миграции, то разработчики реализуют свою кастомную, но суть причины №3 не меняется.

Причины собраны

Причины собраны. Довольно просто их собрать, не правда ли? Ну вот так же просто эти причины собирают многие разработчики, которые один раз вставили код инициализации на заре становления своего проекта и благополучно забыли про него до тех пор пока не увидели в Itunes что-то типа такого:


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

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

Что происходит?

Думаю, все эти причины наталкивают вас на простой вывод. И он верен! Даже простое копирование данных про миграции на новую версию базы данных занимает время, и чем больше занимает файл базы данных занимает, тем больше времени требуется. И мы говорим про простое копирование, а если нужно переиндексировать поля, например?

Узкое место было уже упомянуто — это подключение персистентного хранилища к объекту NSPersistentStoreCoordinator. Именно тут собранные воедино причины создают проблему. И если ваше приложение не отзывается 30 секунд, то система его закрывает.

Решение

На наше счастье создавать NSPersistentStoreCoordinator и подключать к нему хранилище можно в другом потоке. И на момент инициализации данных хороший тон — показать какое-нибудь окошко с надписью «Обновление данных», например.

Вот как это будет выглядеть в коде (сразу скажу, решение не претендует на звание идеального, если кто-то придумает лучше, то пишите в комментариях).


// Выносим весь код инициализации 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 метод, который содержит код, который был у вас в — (BOOL)application:didFinishLaunchingWithOptions:

Контроллер MigrationProgressViewController нужен для отображения, например, индикаторов оставшегося времени или хотя бы показывал, что процесс не повис. Единственная его задача — успокоить пользователя. Пользователям приятнее смотреть пусть даже на «голый» UIActivityIndicatorView, чем на повисший экран заставки и тем более иметь на руках вылетающее приложение.

Это, пожалуй, все. Избегайте подобных стечений обстоятельств и почаще пересматривайте код, особенно тот, что вставил за вас Xcode.

Автор: Silf

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js