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



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

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

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

    Скажу сразу, для того, чтобы встретиться с ошибкой, нужно собрать три причины: большой объем базы, инициализация стека 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.
    image
    Luxoft
    think. create. accelerate.

    Comments 21

      +2
      Можно еще посылать NSNotification, чтобы уведомлять всех заинтересованных сразу об окончании инициализации базы.
        +1
        Именно! Но лучше вообще создание «всех заинтересованных» отложить до момента полной инициализации CoreData. Код будет линейнее и чище. Но если очень надо, то можно от этого правила и отсупить.
          0
          *отступить
          +2
          Главное, чтобы потом не получилось вот так:
          govnokod.ru/9614

          Пардон, наболело :)
            +1
            или не переопределять сеттер — (void)setDataIsReady:(BOOL)dataIsReady и навешивать наблюдателя на изменения свойства dataIsReady, например.
          • UFO just landed and posted this here
              0
              Просто для iPad эта логика немного изменится, нужно будет учитывать и ориентацию устройства, нужно будет сделать переход с эрана заставки на «живой» view незаметным или по крайней мере плавным. Там уже свои пляски с бубном, поэтому лучше вынести всю логику в отдельный класс типа MigrationProgressViewController.
              • UFO just landed and posted this here
                  0
                  Для айпада обычно Launch Images и горизонтальные, и вертикальные. Так что стартануть приложение может в горизонтальной ориентации. Ваш подход на айпад не масштабируется. У вас же просто iphone-приложение?
                  • UFO just landed and posted this here
              +1
              Спасибо, интересный подход, сколько читал и использовал Core Data, не слышал о таком.
              Вопрос в том, насколько срочно необходимо внедрять такой подход, если, например, база всего в несколько мегабайт?
                0
                Если несколько мегабайт, то не стоит беспокоиться. Хотя разработчики как раз пропускают момент «несколько мегабайт->несколько гигабайт» =)
                +2
                Хорошая статья для начинающих, но есть пара змечаний:
                В интернете до сих пор люди задаются вопросом, можно ли хранить картинки в базе данных ...
                

                Хранить большие данные в SQLite как минимум нецелесообразно, так как структура базы может рухнуть. Не преднозначена она для этого особо. Да и скорость доступа из sqlite базы может быть больше из-за фрагментации на страницы, плюс к этому накладывается фрагментация на страницы файлововй системы. Эти две фрагментации не выровняны в sqlite для того, чтоб сохранить хорошую кросс-платформенность.

                Картинки в большинстве случае можно повторно скачать из интернета, поэтому в таких случаях лучше их держать в папке Cache, если кончится место на устройстве операционка может почистить эту папку и освободить пользователю место для чего-нибудь нужного. Это как минимум правило хорошего тона, а как максимум — забота о пользователе.
                А в базе хранить ключь к картинке, и сделать у объекта NSPersistentObject поле которое будет возвращать картинку по этому ключу если картинка есть на диске, плюс можно включить кэширование в памяти, если эти картинки исползуются в UITableView (либо на этом этапе, либо уже в контроллере, если глобальное кэширование не желательно).
                Если же картинки нет — то подгружать ее и сообщать об этом через центр нотификаций, например. Или даже видел реализацию, похожую на Deref которая широко используется в Twisted: — возвращается объект, который либо содержит картинку либо говорит что она тут скоро будет, а когда картинка пришла или произошла ошибка — он дергает метод делегата. Но лично мне решение через нотификационный центр больше нравится — оно «роднее» и проще. Хотя ситуации бывают разные. Да и кроме этих двух реализаций можно придумать еще десяток: делегат у самого объекта, отдельный объект с несколькими очередями загрузки, симуляцию биндинга (почти первый вариант, но более прозрачный)…

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

                Вы так написали, как будто это плохо. Чаще всего в основном его и нужно инициализировать. Если объемы не большие и данные не очень комплексные, то почему бы и нет. Это упростит приложение, ускорит разработку и сделает поддержку приложения более дешевой. Чем сложнее и комплексней приложение — тем дороже оно обходится его владельцу. Более того до iOS5 CoreData был однопоточным и если всю инициализацию стэка положить в отдельный поток ( а в него входит и инициализация NSManagedObjectContext), то для получения данных в основной поток их потребуется дополнительно «защитить». Методов куча, но если этого не учитывать может произойти очень много неприятных и трудноотлавливаемых ошибок.
                Поэтому я настаиваю на том, что 100% профессионалов инициализируют CoreData в основном потоке, если это не вредит приложению.

                И если ваше приложение не отзывается 30 секунд, то система его закрывает.
                

                Время реакции WatchDog'a нигде не указана, но в документации Apple я встречал цифру, что основной поток не должен вешаться более чем на 5 секунд. Но по правильному вообще основной поток вообще не должен вашаться, тоесть для обеспечения нормальной работы один цикл выполнения основного потока не должен превышать трети секунды. Это правило я прочитал в руководстве платформы STi Micro и считаю ее правильной. Есть хорошие набор инструментов, с помощью которого можно это профилировать.
                  +1
                  >Поэтому я настаиваю на том, что 100% профессионалов инициализируют CoreData в основном потоке, если это не вредит приложению.

                  Собственно, про это и статья. Рассмотрена причина, когда это вредит приложению и как просто это «если» забыть, и выпустить бажную версию.
                    0
                    Да, все верно, об этом и речь
                      0
                      фраза звучала не «в 70 процентах случаев» а «70 процентов разработчиков по-опытнее». А это значит что 70 процентов в любом случае будут инициализировать, плохо это или хорошо. А я все-таки считаю, что опытный разработчик должен знать о такой явной особенности CoreData.
                    +1
                    Про картинки автор написал в контексте возможных причин получения объемного файла базы. Причина для кого-то надуманная, для кого-то нет.
                      0
                      Для меня вполне актуальная, так что «надуманность», в данном контексте не уместна =)
                        0
                        а что вы храните в базе?
                      0
                      А можно поподробнее про многопоточность core data в ios5? Первый раз слышу.

                  Only users with full accounts can post comments. Log in, please.