Pułapki korzystania z dwóch stałych koordynatorów sklepu w celu wydajnej aktualizacji tła

Szukam najlepszego możliwego sposobu aktualizacji dość dużego zestawu danych bazujących na danych podstawowych w tle, z jak najmniejszym wpływem na interfejs aplikacji (główny wątek), jak to możliwe.

Jest kilka dobrych materiałów na ten temat, w tym:

Bazując na moich badaniach i osobistym doświadczeniu, najlepszą dostępną opcją jest efektywne wykorzystanie dwóch oddzielnych stosów core-data, które udostępniają dane tylko na poziomie bazy danych (SQLite). Oznacza to, że potrzebujemy dwóch oddzielnych NSPersistentStoreCoordinators, z których każdy ma swój własny NSManagedObjectContext. Z włączoną rejestracją zapisu w bazie danych (domyślnie od iOS 7), konieczność blokowania można uniknąć prawie we wszystkich przypadkach(z wyjątkiem sytuacji, gdy mamy dwa lub więcej jednoczesnych zapisów, co nie jest prawdopodobne w moim scenariuszu).

W celu wydajnej aktualizacji tła i oszczędzania pamięci, należy również przetwarzać dane partiami i okresowo zapisywać kontekst tła, aby brudne obiekty były przechowywane w bazie danych i usuwane z pamięci. Można użyć NSManagedObjectContextDidSaveNotification, który jest generowany w tym momencie, aby połączyć zmiany tła z głównym kontekstem, ale w ogólne nie chcesz aktualizować interfejsu natychmiast po zapisaniu partii. Chcesz poczekać, aż zadanie w tle zostanie całkowicie wykonane, a następnie odświeżyć interfejs użytkownika (zalecane zarówno w sesji WWDC, jak i objc.io artykuły). Oznacza to, że główny kontekst aplikacji pozostaje przez pewien czas poza synchronizacją z bazą danych.

To wszystko prowadzi mnie do mojego głównego pytania, które brzmi: co może pójść nie tak, jeśli zmienię bazę danych w ten sposób, bez natychmiastowego powiedzenie głównemu kontekstowi scalania zmian? zakładam, że to nie tylko słońce i róże.

Jeden konkretny scenariusz, który mam w głowie, to, co się stanie, jeśli błąd musi być spełniony dla obiektu załadowanego w głównym kontekście, jeśli operacja w tle usunęła ten obiekt z bazy danych? Czy może się to zdarzyć na przykład w widoku tabeli nsfetchedresultscontroller, który używa rozmiaru batchSize do pobierania obiektów stopniowo do pamięci? Czyli obiekt, który nie jednak został w pełni pobrany zostanie usunięty, ale następnie przewijamy do punktu, w którym obiekt musi zostać załadowany. Czy to potencjalny problem? Czy inne rzeczy mogą pójść nie tak? Będę wdzięczny za wszelkie uwagi w tej sprawie.

Author: Matej Bukovinski, 2013-10-10

4 answers

Świetne pytanie!

Tzn. obiekt, który nie został jeszcze w pełni pobrany zostanie usunięty, ale następnie przewijamy do punktu, w którym obiekt musi zostać załadowany. Na to potencjalny problem?

Niestety to spowoduje problemy. Zostanie wyrzucony następujący wyjątek:

Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xc544570 <x-coredata://(...)>'

Ten wpis na blogu (sekcja zatytułowana "jak zrobić współbieżność z podstawowymi danymi?") może być nieco pomocny, ale nie wyczerpuje tego tematu. Walczę z tym samym problemy w aplikacji pracuję teraz i chciałbym przeczytać zapis o tym.

 5
Author: Arek Holko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-10 13:44:23

Bazując na twoim pytaniu, komentarzach i moim własnym doświadczeniu, wydaje się, że większym problemem, który próbujesz rozwiązać, jest: 1. Używanie kontrolera nsfetchedresultscontroller na głównym wątku z ograniczeniem wątku 2. Importowanie dużego zestawu danych, który wstawia, aktualizuje lub usuwa zarządzane obiekty w kontekście. 3. Import powoduje, że duże powiadomienia scalania są przetwarzane przez główny wątek w celu aktualizacji interfejsu użytkownika. 4. Duże połączenie ma kilka możliwych efektów: - Interfejs staje się powolny lub zbyt zajęty, aby mógł być użyteczny. Może to być spowodowane używaniem beginUpdates/endUpdates aby zaktualizować widok tableview w NSFetchedResultsControllerDelegate, i masz wiele animacji quiing up z powodu dużego scalenia. - Użytkownicy mogą napotkać "nie udało się spełnić błędu", gdy próbują uzyskać dostęp do uszkodzonego obiektu, który został usunięty ze sklepu. Kontekst zarządzanego obiektu myśli, że istnieje, ale kiedy trafia do sklepu w celu spełnienia błędu, Błąd został już usunięty. Jeśli jesteś używając reloadData do aktualizacji widoku tabeli w kontrolerze NSFetchedResultsControllerDelegate, jest bardziej prawdopodobne, że to się stanie niż podczas używania beginUpdates/endUpdates.

Podejście, które próbujesz wykorzystać do rozwiązania powyższych problemów jest: - Utwórz dwa NSPersistentStoreCoordinators, każdy dołączony do tego samego NSPersistentStore lub co najmniej ten sam adres URL pliku nspersistentstore SQLite. - Twój import następuje na NSManagedObjectContext 1, dołączonym do NSPersistentStoreCoordinator 1, oraz wykonanie na innym wątku (wątkach). Twój kontroler NSFetchedResultsController używa NSManagedObjectContext 2, dołączonego do NSPersistentStoreCoordinator 2, działającego w głównym wątku. - Przenosisz zmiany z NSManagedObjectContext 1 na 2

Napotkasz kilka problemów z tym podejściem. - An NSPersistentStoreCoordinator ' S zadaniem jest pośredniczenie pomiędzy dołączonymi do niego NSManagedObjectContexts a dołączonymi do niego magazynami. W scenariusz multiple-Coordinator-context, który opisujesz, zmiany w bazowym sklepie przez NSManagedObjectContext 1, które powodują zmianę w pliku SQLite, nie będą widoczne przez NSPersistentStoreCoordinator 2 i jego kontekst. 2 nie wie, że 1 zmienił plik, a będziesz miał "nie mógł spełnić błędu" i inne ekscytujące wyjątki. - Nadal będziesz musiał w pewnym momencie umieścić zmienione NSManagedObjects z importu do nsmanagedobjectcontext 2. Jeśli te zmiany są duże, możesz nadal będzie miał problemy z interfejsem użytkownika, a interfejs będzie zsynchronizowany ze sklepem, co może prowadzić do "nie udało się spełnić błędu". - W ogóle, ponieważ NSManagedObjectContext 2 nie używa tego samego NSPersistentStoreCoordinator jak NSManagedObjectContext 1, będziesz miał problemy z rzeczy są synchronizowane. Nie tak te rzeczy mają być używane razem. Jeśli importujesz i zapisujesz w NSManagedObjectContext 1, NSManagedObjectContext 2 jest natychmiast w stanie niezgodnym z sklep.

Oto niektóre z rzeczy, które mogą pójść nie tak z tym podejściem. Większość z tych problemów będzie widoczna podczas odpalania usterki, ponieważ dostęp do sklepu. Więcej na temat działania tego procesu można przeczytać w Core Data Programming Guide, natomiast Incremental Store Programming Guide opisuje proces bardziej szczegółowo. Sklep SQLite postępuje zgodnie z tym samym procesem, co przyrostowa implementacja sklepu.

Znowu przypadek użycia opisujesz-pozyskiwanie mnóstwa nowych danych, wykonywanie find-Or-Create Na danych w celu tworzenia lub aktualizacji zarządzanych obiektów oraz usuwanie "starych" obiektów, które mogą w rzeczywistości stanowić większość sklepu-to coś, z czym mam do czynienia każdego dnia od kilku lat, widząc wszystkie te same problemy, które Ty masz. Istnieją rozwiązania-nawet w przypadku importu, który zmienia 60 000 złożonych obiektów na raz, a nawet przy użyciu zamknięcia wątku! - ale to nie wchodzi w zakres twojego pytania. (Podpowiedź: Konteksty rodzic-dziecko nie wymagają scalania powiadomień).

 3
Author: quellish,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-16 19:58:45

Dwóch stałych koordynatorów sklepów (PSC) jest z pewnością drogą do dużych zbiorów danych. Blokowanie plików jest szybsze niż blokowanie podstawowych danych.

Nie ma powodu, abyś nie mógł użyć psc w tle do utworzenia wątku ograniczonego NSManagedObjectContexts, w którym każdy jest tworzony dla każdej operacji wykonywanej w tle. Jednak zamiast pozwolić core data zarządzać kolejką, musisz teraz utworzyć NSOperationQueues i / lub wątki do zarządzania operacjami w oparciu o to, co robisz to w tle. Nsmanagedobjectcontext są bezpłatne i nie są drogie. Gdy to zrobisz, możesz zawiesić swój NSManagedObjectContext i używać go tylko podczas tej jednej operacji i / lub czasu życia wątków i zbudować tyle zmian, ile chcesz i poczekać do końca, aby zatwierdzić je i scalić je do głównego wątku, jak kiedykolwiek zdecydujesz. Nawet jeśli masz jakieś główne wątki, możesz nadal w kluczowych punktach w czasie życia operacji refetch / scalić z powrotem do kontekstu wątków.

Ważne jest również, aby wiedzieć, że jeśli pracujesz na dużych zestawach danych, nie martw się o łączenie kontekstów, tak długo, jak nie dotykasz czegoś innego. Na przykład, jeśli masz klasę A i klasę B i masz dwie oddzielne operacje / wątki do pracy nad nimi i nie mają one bezpośredniego związku, nie musisz łączyć kontekstów, jeśli jeden się zmieni, możesz kontynuować toczenie ze zmianami. Jedyną istotną potrzebą łączenia kontekstów tła w ten sposób jest to, że istnieją bezpośrednie związki. Lepiej byłoby temu zapobiec poprzez jakąś serializację, czy to NSOperationQueue, czy co innego. Więc nie krępuj się pracować na różnych obiektach w tle, po prostu uważaj na ich relacje.

Pracowałem nad dużymi projektami core data i ten wzór działał bardzo dobrze dla mnie.

 3
Author: rfrittelli,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-17 19:24:07

Rzeczywiście, jest to najlepszy scenariusz baz danych, z którym możesz pracować. Prawie brak głównego interfejsu użytkownika i łatwe zarządzanie danymi w tle. Jeśli chcesz podać główny kontekst (i być może aktualnie działający NSFetchedResultsController), słuchasz powiadomień o zapisie tła w następujący sposób:

    [[NSNotificationCenter defaultCenter] 
      addObserver:self selector:@selector(reloadFetchedResults:)
      name:NSManagedObjectContextDidSaveNotification
      object:backgroundObjectContext];

Następnie, można scalić zmiany, ale czekając na kontekst głównego wątku, aby je złapać przed zapisaniem . Po otrzymaniu powiadomienia mergeChangesFromContextDidSaveNotification zmiany nie są jeszcze zapisywane. Stąd {[4] } jest obowiązkowe, więc główny kontekst pobiera zmiany, a następnie NSFetchedResultsController poprawnie aktualizuje swoje wartości.

-(void)reloadFetchedResults:(NSNotification*)notification
{
    NSManagedObjectContext*moc=[notification object];
    if ([moc isEqual:backgroundObjectContext]) 
    {
        // Delete caches of fethcedResults if you have a deletion
        if ([[theNotification.userInfo objectForKey:NSDeletedObjectsKey] count]) {
            [NSFetchedResultsController deleteCacheWithName:nil];
         }
        // Block the background execution of the save, and merge changes before
        [managedObjectContext performBlockandWait:^{
            [managedObjectContext 
            mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}
Jest pułapka, której nikt nie zauważył. możesz otrzymać powiadomienie o zapisaniu, zanim kontekst tła rzeczywiście zapisze obiekt, który chcesz scalić . Jeśli chcesz uniknąć problemów przez szybszy kontekst główny z pytaniem o obiekt, który nie został jeszcze zapisany przez kontekst tła, powinieneś (you really should) wywołać obtainPermanentIDsForObjects przed dowolny zapis tła. Więc możesz zadzwonić do mergeChangesFromContextDidSaveNotification. Zapewni to, że scalanie otrzyma ważny stały identyfikator do scalania.
 1
Author: Pablo Romeu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-22 14:44:57