Впровадження швидкого та ефективного імпорту основних даних на iOS 5


101

Питання : Як я можу дозволити моєму дочірньому контексту побачити зміни, які зберігаються у батьківському контексті, щоб вони викликали мій NSFetchedResultsController для оновлення інтерфейсу користувача?

Ось налаштування:

У вас є додаток, яке завантажує та додає багато XML-даних (близько 2 мільйонів записів, кожна приблизно розміром із звичайного абзацу тексту) .sqlite-файл має розмір близько 500 Мб. Додавання цього вмісту до основних даних потребує часу, але ви хочете, щоб користувач міг користуватися програмою, тоді як дані поступово завантажуються в сховище даних. Користувачеві повинно бути непомітним і непомітним, що велика кількість даних переміщується навколо, так що ніяких зависань, жодних тремтінь: прокрутки, як масло. Але додаток тим корисніше, чим більше даних додається до нього, тому ми не можемо вічно чекати, коли дані будуть додані до магазину основних даних. У коді це означає, що я дуже хотів би уникати такого коду у коді імпорту:

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];

Додаток - лише iOS 5, тому найповільнішим пристроєм, який він потребує, є iPhone 3GS.

Ось ресурси, які я до цього часу використовував для розробки свого поточного рішення:

Основний посібник з програмування даних Apple: ефективний імпорт даних

  • Використовуйте пули автоматичного випуску, щоб зберегти пам'ять
  • Вартість відносин. Імпортуйте квартиру, а потім закріпіть відносини в кінці
  • Не запитуйте, чи можете ви допомогти, це сповільнює ситуацію O (n ^ 2)
  • Імпорт партіями: зберегти, скинути, злити та повторити
  • Вимкніть диспетчер скасування при імпорті

iDeveloper TV - продуктивність основних даних

  • Використовуйте 3 контексти: контекст основного, головного та конфілігентного типів

iDeveloper TV - основні дані для оновлення Mac, iPhone та iPad

  • Запуск економить на інших чергах за допомогою performBlock швидко покращує роботу.
  • Шифрування сповільнює роботу, виключайте, якщо можете.

Імпорт та показ великих наборів даних у основних даних Маркус Зарра

  • Ви можете уповільнити імпорт, надавши час поточному циклу запуску, щоб користувачі не почували себе гладко.
  • Зразок коду доводить, що можна імпортувати великі імпорти та підтримувати інтерфейс інтерпретації, але не так швидко, як з 3-ма контекстами та збереженням асинхронізації на диску.

Моє поточне рішення

У мене є 3 екземпляри NSManagedObjectContext:

masterManagedObjectContext - це контекст, який має NSPersistentStoreCoordinator і відповідає за збереження на диску. Я роблю це, щоб мої заощадження могли бути асинхронними і тому дуже швидкими. Я створюю його при запуску так:

masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];

mainManagedObjectContext - це контекст, який користувальницький інтерфейс використовує всюди. Це дочірка masterManagedObjectContext. Я створюю це так:

mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];

backgroundContext - Цей контекст створений у моєму підкласі NSOperation, який відповідає за імпорт XML-даних у основні дані. Я створюю його в основному методі операції і пов'язую його з головним контекстом.

backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];

Це насправді працює дуже, ДУЖЕ швидко. Тільки зробивши цю 3 конфігурацію контексту, я зміг покращити швидкість імпорту понад 10 разів! Чесно кажучи, у це важко повірити. (Цей базовий дизайн повинен бути частиною стандартного шаблону основних даних ...)

Під час імпортування я зберігаю два різні способи. Кожні 1000 елементів, які я зберігаю у фоновому контексті:

BOOL saveSuccess = [backgroundContext save:&error];

Тоді в кінці процесу імпорту я зберігаю на контексті master / parent, який, нібито, виштовхує модифікації до інших дочірніх контекстів, включаючи основний контекст:

[masterManagedObjectContext performBlock:^{
   NSError *parentContextError = nil;
   BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];

Проблема : проблема полягає в тому, що мій інтерфейс не оновиться, поки я не перезавантажую представлення даних.

У мене є простий UIViewController з UITableView, який подається даними за допомогою NSFetchedResultsController. Коли процес імпорту завершується, NSFetchedResultsController бачить відсутність змін у батьківському / головному контексті, і тому інтерфейс користувача не оновлюється автоматично, як я звик бачити. Якщо я вискочу UIViewController зі стека та завантажую його знову всі дані є.

Питання : Як я можу дозволити моєму дочірньому контексту побачити зміни, які зберігаються у батьківському контексті, щоб вони викликали мій NSFetchedResultsController для оновлення інтерфейсу користувача?

Я спробував наступне, що просто висить додаток:

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    NSError *error = nil;
    BOOL saveSuccess = [masterManagedObjectContext save:&error];

    [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}

- (void)contextChanged:(NSNotification*)notification
{
    if ([notification object] == mainManagedObjectContext) return;

    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(contextChanged:) withObject:notification waitUntilDone:YES];
        return;
    }

    [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}

26
+1000000 за найкраще сформоване, найбільш підготовлене запитання. У мене теж є відповідь ... Хоча це набере кілька хвилин,
Джоді Хейґінс

1
Коли ви кажете, що додаток підвішено, де це? Що це робить?
Джоді Хейґінс

Вибачте, що довели це через тривалий час. Чи можете ви уточнити, що означає "Імпортувати квартиру, а потім закріпити відносини в кінці"? Чи вам ще не потрібно мати ці об’єкти в пам'яті, щоб встановити стосунки? Я намагаюся реалізувати рішення, подібне до вашого, і я міг би реально використати допомогу, щоб знизити слід пам'яті.
Андреа Шпрега

Див. Документи Apple, пов’язані з першою у цій статті. Це пояснює це. Удачі!
Девід Вайс

1
Дійсно гарне запитання, і я підібрав кілька акуратних хитрощів із опису, який ви надали для своєї установки
djskinner

Відповіді:


47

Ви, ймовірно, також повинні зберегти головний МОК у кроці. Немає сенсу, щоб MOC чекав до кінця, щоб зберегти. У нього є власна нитка, і це також допоможе зберегти пам'ять.

Ти написав:

Тоді в кінці процесу імпорту я зберігаю на контексті master / parent, який, нібито, виштовхує модифікації до інших дочірніх контекстів, включаючи основний контекст:

У вашій конфігурації у вас є двоє дітей (основний MOC і фоновий MOC), обидва відведені "майстру".

Коли ви економите на дитині, це підштовхує зміни до батьків. Інші діти цього МОК побачать ці дані наступного разу, коли вони виберуть ... про них явно не повідомляється.

Отже, коли BG зберігає, його дані висуваються на MASTER. Однак зауважте, що жоден з цих даних не знаходиться на диску, поки MASTER не збереже. Крім того, будь-які нові елементи не отримають постійні ідентифікатори, поки MASTER не збережеться на диску.

У вашому сценарії ви перетягуєте дані в ОСНОВНУ МОК, об'єднуючись із збереженого МАСТЕРУ під час сповіщення DidSave.

Це повинно працювати, тому мені цікаво, де це "висить". Зауважу, що ви не працюєте на головній потоці MOC канонічним чином (принаймні, не для iOS 5).

Крім того, ви, мабуть, зацікавлені лише в об'єднанні змін з головним MOC (хоча ваша реєстрація виглядає так, як це все одно). Якби я використовував сповіщення update-on-did-save, я зробив би це ...

- (void)contextChanged:(NSNotification*)notification {
    // Only interested in merging from master into main.
    if ([notification object] != masterManagedObjectContext) return;

    [mainManagedObjectContext performBlock:^{
        [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];

        // NOTE: our MOC should not be updated, but we need to reload the data as well
    }];
}

Тепер, що може бути вашим справжнім питанням щодо повішення ..., ви показуєте два різні дзвінки, щоб зберегти майстер. перший добре захищений у власному виконанніBlock, але другий - ні (хоча ви можете викликати saveMasterContext у performBlock ...

Однак я б також змінив цей код ...

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    // Make sure the master runs in it's own thread...
    [masterManagedObjectContext performBlock:^{
        NSError *error = nil;
        BOOL saveSuccess = [masterManagedObjectContext save:&error];
        // Handle error...
        [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
    }];
}

Однак зауважте, що ГОЛОВНА є дитиною MASTER. Отже, зміни не повинні були зливатися. Замість цього просто слідкуйте за програмою DidSave на майстрі та просто перезавантажте! Дані вже сидять у вашого батька, просто чекаючи, коли ви попросите їх. Це одна з переваг наявності даних у батьківській програмі в першу чергу.

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

Замість того, щоб зробити фоновий МОК дитиною МАЙСТЕРА, зробіть його дитиною ОСНОВНОГО.

Зробіть це. Кожен раз, коли BG зберігає, він автоматично натискається на ОСНОВНЕ. Тепер ГОЛОВНИК повинен зателефонувати зберегти, і тоді майстер повинен зателефонувати зберегти, але все, що робить, це переміщення покажчиків ..., поки майстер не збереже на диск.

Краса цього методу полягає в тому, що дані переходять з фонового MOC прямо у MOC ваших програм (потім передаються, щоб їх зберегти).

Існує певна кара за прохід, але весь важкий підйом робиться в МАЙСТЕР, коли він потрапляє на диск. І якщо ви натисните ці збережені на майстер за допомогою performBlock, то головна нитка просто відправляє запит і повертається негайно.

Будь ласка, дайте мені знати, як це йде!


Відмінна відповідь. Я спробую ці ідеї сьогодні і побачу, що я відкрию. Дякую!
Девід Вайс

Дивовижно! Це спрацювало чудово! І все-таки я спробую запропонувати вашу пропозицію MASTER -> MAIN -> BG і побачити, як працює ця вистава, це здається дуже цікавою ідеєю. Дякую за чудові ідеї!
Девід Вайс

4
Оновлено, щоб змінити performBlockAndЗачекайте виконанняBlock. Не впевнений, чому це знову спливе в моїй черзі, але коли я прочитав це цього разу, було очевидно ... не впевнений, чому я раніше його відпустив. Так, виконайте повторне вступ у програму PerformBlockAndWait. Однак у подібному вкладеному середовищі не можна викликати синхронну версію в дочірньому контексті з батьківського контексту. Повідомлення може бути (у даному випадку є) надіслане з батьківського контексту, що може спричинити тупик. Я сподіваюся, що це зрозуміло кожному, хто піде разом і прочитає це пізніше. Спасибі, Девіде.
Джоді Хейґінс

1
@DavidWeiss Ви пробували MASTER -> MAIN -> BG? Мене цікавить ця модель дизайну і сподіваюся дізнатися, чи добре вона працює для вас. Дякую.
nonamelive

2
Проблема з MASTER -> MAIN -> Шаблон BG - коли ви отримуєте з контексту BG, він також отримуватиме від MAIN, і це блокує інтерфейс користувача та змусить вас додаток не реагувати
Ростислав
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.