Wdrażanie szybkiego i wydajnego importu danych podstawowych na iOS 5

101

Pytanie : Jak sprawić, aby mój kontekst podrzędny zobaczył zmiany utrwalone w kontekście nadrzędnym, tak aby wyzwalały one mój NSFetchedResultsController w celu zaktualizowania interfejsu użytkownika?

Oto konfiguracja:

Masz aplikację, która pobiera i dodaje dużo danych XML (około 2 milionów rekordów, każdy mniej więcej rozmiar normalnego akapitu tekstu). Plik .sqlite ma rozmiar około 500 MB. Dodanie tej zawartości do danych podstawowych wymaga czasu, ale chcesz, aby użytkownik mógł korzystać z aplikacji, podczas gdy dane są ładowane do magazynu danych przyrostowo. Musi być niewidoczne i niezauważalne dla użytkownika, że ​​przenoszone są duże ilości danych, więc nie zawiesza się, nie drży: zwoje jak masło. Mimo to aplikacja jest bardziej użyteczna, im więcej danych jest do niej dodawanych, więc nie możemy czekać w nieskończoność na dodanie danych do magazynu Core Data. W kodzie oznacza to, że naprawdę chciałbym uniknąć takiego kodu w kodzie importu:

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

Aplikacja działa tylko w systemie iOS 5, więc najwolniejszym urządzeniem, które musi obsługiwać, jest iPhone 3GS.

Oto zasoby, z których do tej pory korzystałem, aby opracować moje obecne rozwiązanie:

Podręcznik programowania podstawowych danych firmy Apple: wydajne importowanie danych

  • Użyj puli Autorelease, aby zmniejszyć ilość pamięci
  • Koszt relacji. Importuj na płasko, a na końcu popraw relacje
  • Nie pytaj, czy możesz temu pomóc, spowalnia to wszystko w sposób O (n ^ 2)
  • Importuj partiami: zapisz, zresetuj, opróżnij i powtórz
  • Wyłącz Menedżera cofania podczas importu

iDeveloper TV - Wydajność podstawowych danych

  • Użyj 3 kontekstów: kontekstów głównych, głównych i zamkniętych

iDeveloper TV - Aktualizacja podstawowych danych dla komputerów Mac, iPhone i iPad

  • Uruchamianie zapisuje na innych kolejkach z performBlock przyspiesza pracę.
  • Szyfrowanie spowalnia działanie, wyłącz je, jeśli możesz.

Importowanie i wyświetlanie dużych zestawów danych w danych podstawowych autorstwa Marcusa Zarry

  • Możesz spowolnić import, dając czas na bieżącą pętlę uruchamiania, aby użytkownik czuł się płynnie.
  • Przykładowy kod udowadnia, że ​​można wykonywać duże importy i utrzymywać responsywny interfejs użytkownika, ale nie tak szybko, jak w przypadku 3 kontekstów i asynchronicznego zapisywania na dysku.

Moje obecne rozwiązanie

Mam 3 wystąpienia NSManagedObjectContext:

masterManagedObjectContext - jest to kontekst, który ma NSPersistentStoreCoordinator i jest odpowiedzialny za zapisywanie na dysku. Robię to, aby moje zapisy były asynchroniczne, a przez to bardzo szybkie. Tworzę go przy uruchomieniu w ten sposób:

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

mainManagedObjectContext - jest to kontekst, którego interfejs użytkownika używa wszędzie. Jest elementem potomnym masterManagedObjectContext. Tworzę to tak:

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

backgroundContext - ten kontekst jest tworzony w mojej podklasie NSOperation, która jest odpowiedzialna za importowanie danych XML do Core Data. Tworzę go w głównej metodzie operacji i łączę tam z głównym kontekstem.

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

To faktycznie działa bardzo, BARDZO szybko. Po prostu wykonując tę ​​konfigurację 3 kontekstów, udało mi się zwiększyć prędkość importu ponad 10 razy! Szczerze mówiąc, trudno w to uwierzyć. (Ten podstawowy projekt powinien być częścią standardowego szablonu podstawowych danych ...)

Podczas importu zapisuję 2 różne sposoby. Co 1000 pozycji zapisuję w kontekście tła:

BOOL saveSuccess = [backgroundContext save:&error];

Następnie pod koniec procesu importu zapisuję w kontekście głównym / nadrzędnym, który rzekomo wypycha modyfikacje do innych kontekstów potomnych, w tym głównego kontekstu:

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

Problem : Problem polega na tym, że mój interfejs użytkownika nie zaktualizuje się, dopóki ponownie nie załaduję widoku.

Mam prosty UIViewController z UITableView, który jest podawany danych przy użyciu NSFetchedResultsController. Po zakończeniu procesu importu NSFetchedResultsController nie widzi żadnych zmian z kontekstu nadrzędnego / głównego, a więc interfejs użytkownika nie aktualizuje się automatycznie, tak jak zwykle. Jeśli zdejmę UIViewController ze stosu i załaduję go ponownie, wszystkie dane tam są.

Pytanie : Jak sprawić, aby mój kontekst podrzędny zobaczył zmiany utrwalone w kontekście nadrzędnym, tak aby wyzwalały one mój NSFetchedResultsController w celu zaktualizowania interfejsu użytkownika?

Wypróbowałem następujące rozwiązanie, które po prostu zawiesza aplikację:

- (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];
}
David Weiss
źródło
26
+1000000 za najlepiej sformułowane, najlepiej przygotowane pytanie w historii. Ja też mam odpowiedź ... Jednak zapisanie jej zajmie kilka minut ...
Jody Hagins
1
Kiedy mówisz, że aplikacja się zawiesiła, gdzie to jest? Co to robi?
Jody Hagins
Przepraszam, że poruszam to po długim czasie. Czy możesz wyjaśnić, co oznacza „Importuj na płasko, a następnie popraw relacje na końcu”? Czy nadal nie musisz mieć tych obiektów w pamięci, aby nawiązać relacje? Próbuję wdrożyć rozwiązanie bardzo podobne do twojego i naprawdę przydałaby mi się pomoc w zmniejszeniu zużycia pamięci.
Andrea Sprega
Zobacz Dokumenty Apple połączone z pierwszym artykułem w tym artykule. To wyjaśnia to. Powodzenia!
David Weiss
1
Naprawdę dobre pytanie i wybrałem kilka fajnych sztuczek z opisu, który
podałeś

Odpowiedzi:

47

Prawdopodobnie powinieneś również szybko zapisać nadrzędny MOC. Nie ma sensu, aby MOC czekał do końca, aby zapisać. Ma swój własny wątek i pomoże również utrzymać pamięć.

Napisałeś:

Następnie pod koniec procesu importu zapisuję w kontekście głównym / nadrzędnym, który rzekomo wypycha modyfikacje do innych kontekstów potomnych, w tym głównego kontekstu:

W twojej konfiguracji masz dwoje dzieci (główny MOC i MOC działający w tle), oba są rodzicami „mastera”.

Kiedy oszczędzasz na dziecku, wypycha zmiany do rodzica. Inne elementy podrzędne tego MOC zobaczą dane przy następnym pobieraniu ... nie są jawnie powiadamiane.

Tak więc, kiedy BG zapisuje, jego dane są przesyłane do MASTER. Należy jednak pamiętać, że żadne z tych danych nie znajdują się na dysku, dopóki MASTER nie zapisze. Co więcej, żadne nowe elementy nie otrzymają trwałych identyfikatorów, dopóki MASTER nie zapisze ich na dysku.

W twoim scenariuszu pobierasz dane do MAIN MOC poprzez scalenie z MASTER save podczas powiadomienia DidSave.

To powinno zadziałać, więc jestem ciekawy, gdzie jest „zawieszony”. Zaznaczę, że nie uruchamiasz się na głównym wątku MOC w sposób kanoniczny (przynajmniej nie na iOS 5).

Ponadto prawdopodobnie jesteś zainteresowany tylko scaleniem zmian z głównego MOC (chociaż twoja rejestracja wygląda tak, jakby była tylko do tego). Gdybym miał użyć powiadomienia o aktualizacji przy zapisywaniu, zrobiłbym to ...

- (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
    }];
}

Teraz, co może być twoim prawdziwym problemem związanym z zawieszaniem się ... pokazujesz dwa różne wywołania, aby zapisać na master. pierwsza jest dobrze chroniona w swoim własnym performBlock, ale druga nie (chociaż możesz wywoływać saveMasterContext w performBlock ...

Jednak zmieniłbym też ten kod ...

- (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];
    }];
}

Należy jednak pamiętać, że MAIN jest dzieckiem MASTER. Więc nie powinno być konieczne scalanie zmian. Zamiast tego po prostu obserwuj DidSave na master i po prostu ponów! Dane są już w Twoim rodzicu i tylko czekają, aż o nie poprosisz. To przede wszystkim jedna z zalet posiadania danych u rodzica.

Kolejna alternatywa do rozważenia (a chciałbym usłyszeć o twoich wynikach - to dużo danych) ...

Zamiast uczynić tło MOC dzieckiem MISTRZA, uczyń go dzieckiem GŁÓWNEGO.

Uzyskać to. Za każdym razem, gdy BG zapisuje, jest automatycznie wpychany do GŁÓWNEGO. Teraz MAIN musi wywołać save, a następnie master musi wywołać save, ale wszystko co robią to przesuwanie wskaźników ... aż master zapisze na dysk.

Piękno tej metody polega na tym, że dane są przesyłane z MOC w tle bezpośrednio do MOC aplikacji (a następnie przechodzą do zapisania).

Jest pewna kara za przejście, ale całe ciężkie podnoszenie jest wykonywane w MASTER, gdy uderza w dysk. A jeśli wyrzucisz te zapisy z mastera za pomocą performBlock, główny wątek po prostu wyśle ​​żądanie i natychmiast wróci.

Daj mi znać, jak leci!

Jody Hagins
źródło
Doskonała odpowiedź. Spróbuję dzisiaj tych pomysłów i zobaczę, co odkryję. Dziękuję Ci!
David Weiss
Niesamowite! Działało idealnie! Mimo to spróbuję Waszej propozycji MASTER -> MAIN -> BG i zobaczę, jak to działa, to bardzo ciekawy pomysł. Dziękuję za świetne pomysły!
David Weiss
4
Zaktualizowano, aby zmienić performBlockAndWait na performBlock. Nie jestem pewien, dlaczego pojawiło się to ponownie w mojej kolejce, ale kiedy przeczytałem to tym razem, było oczywiste ... nie wiem, dlaczego wcześniej odpuściłem. Tak, performBlockAndWait jest ponownie uczestnikiem. Jednak w takim zagnieżdżonym środowisku nie można wywołać wersji synchronicznej w kontekście podrzędnym z kontekstu nadrzędnego. Powiadomienie może być (w tym przypadku jest) wysyłane z kontekstu nadrzędnego, co może spowodować zakleszczenie. Mam nadzieję, że jest to jasne dla każdego, kto przyjdzie i przeczyta to później. Dzięki, David.
Jody Hagins,
1
@DavidWeiss Czy próbowałeś MASTER -> MAIN -> BG? Interesuje mnie ten wzorzec projektowy i mam nadzieję, że wiem, czy dobrze Ci pasuje. Dziękuję Ci.
nonamelive
2
Problem z wzorcem MASTER -> MAIN -> BG polega na tym, że podczas pobierania z kontekstu BG pobiera się również z MAIN, co zablokuje interfejs użytkownika i sprawi, że aplikacja nie będzie reagować
Rostyslav