Pytanie Kontekst obiektu zarządzanego w tle Staggers Animacja interfejsu użytkownika


Mam drzemiący problem, nad którym pracuję od kilku tygodni. Wiąże się to z hamowaniem wydajności UI za każdym razem, gdy zapisuję kontekst obiektu zarządzanego Core Data. Zrobiłem wszystko, o ile mogłem, na własną rękę i szukam pomocy.

Sytuacja

Moja aplikacja używa dwóch NSManagedObjectContext instancje. Jeden należy do delegata aplikacji i ma do niego dołączonego stałego koordynatora sklepu. Drugi jest dzieckiem głównego MOC i należy do Class obiekt, zwany PhotoFetcher. To używa NSPrivateQueueConcurrencyType więc wszystkie operacje wykonywane na tym MOC odbywają się w kolejce w tle.

Nasza aplikacja pobiera dane JSON reprezentujące dane o zdjęciach z naszego API. Aby pobrać dane z naszego interfejsu API, następuje następująca sekwencja kroków:

  1. Zbuduj NSURLRequest obiekt i użyj NSURLConnectionDataDelegate protokół do konstruowania danych zwróconych z żądania lub obsługi błędów.
  2. Po zakończeniu pobierania danych JSON, wykonaj blok w kolejce drugiego podrzędnego MOC, który wykonuje następujące czynności:
    1. Przetwórz JSON używając NSJSONSerialization do instancji klasy Foundation.
    2. Iteruj nad analizowanymi danymi, wstawiając lub aktualizując elementy w moim kontekście kontekstowym, w razie potrzeby. Zazwyczaj powoduje to około 300 nowych lub zaktualizowanych elementów.
    3. Zapisz kontekst tła. To propaguje moje zmiany do głównego MOC.
    4. Wykonaj blok na głównym MOC, aby zapisać jego kontekst. Powoduje to, że nasze dane są przechowywane na dysku, a SQLite sklep. Na koniec wykonaj wywołanie zwrotne dla delegata, informując go, że odpowiedź została w całości umieszczona w magazynie danych podstawowych.

Kod do zachowania tła MOC wygląda mniej więcej tak:

[AppDelegate.managedObjectContext performBlock:^{
    [AppDelegate saveContext]; //A standard save: call to the main MOC
}];

Podczas zapisywania kontekstu głównego obiektu, zapisuje on również dużą liczbę plików JPEG, które zostały pobrane od czasu ostatniego zapisu kontekstu obiektu głównego. Obecnie na iPhonie 4 pobieramy 15 200 x 200 JPEG przy 70% kompresji lub około 2 MB danych w sumie.

Problem

To działa i działa dobrze. Mój problem polega na tym, że raz kontekst tła zapisuje, NSFetchedResultsController W moim widoku kontroler odbiera zmiany propagowane do głównego MOC. Wstawia nowe komórki do naszej PSTCollectionView, klon o otwartym kodzie źródłowym UICollectionView. Podczas wstawiania nowych komórek główny kontekst zapisuje i zapisuje te zmiany na dysku. Może to zająć na iPhonie 4 z systemem iOS 5.1, w dowolnym miejscu od 250-350ms.

W tej jednej trzeciej sekundy aplikacja zupełnie nie odpowiada. Animacje, które były w toku przed zapisaniem, są wstrzymywane i żadne nowe zdarzenia użytkownika nie są wysyłane do głównej pętli, dopóki nie zakończy się zapis.

Uruchomiłem naszą aplikację w Instruments za pomocą narzędzia Time Profiler, aby określić, co blokuje nasz główny wątek. Niestety wyniki były raczej nieprzejrzyste. To najcięższy ślad stosu jaki otrzymałem od Instruments.

Instruments Heaviest Stack Trace

To pojawiło się do zapisywania aktualizacji w magazynie trwałym, ale nie mogłem być tego pewien. Dlatego usunąłem wszystkie połączenia do saveContextw ogóle MOC nie dotknie dysku, a blokowanie wywołania głównego wątku nadal trwa.

Ślad w formie tekstowej wygląda następująco:

Symbol Name
-[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:]
 -[NSManagedObjectContext executeFetchRequest:error:]
  -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]
   _perform
    _dispatch_barrier_sync_f_invoke
     _dispatch_client_callout
      __82-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]_block_invoke_0
       -[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:]
        -[NSManagedObjectContext executeFetchRequest:error:]
         -[NSPersistentStoreCoordinator executeRequest:withContext:error:]
          -[NSSQLCore executeRequest:withContext:error:]
           -[NSSQLCore objectsForFetchRequest:inContext:]
            -[NSSQLCore newRowsForFetchPlan:]
             -[NSSQLCore _newRowsForFetchPlan:selectedBy:withArgument:]
              -[NSSQLiteConnection execute]

Co próbowałem

Zanim dotknęliśmy kodu Core Data, pierwszą rzeczą, jaką zrobiliśmy, była optymalizacja naszych plików JPEG. Przełączyliśmy się na mniejsze JPEG i zobaczyliśmy wzrost wydajności. Następnie zmniejszyliśmy liczbę pobieranych jednocześnie plików JPEG (z 90 do 15). To również prowadzi do znacznego zwiększenia wydajności. Jednak nadal widzimy bloki o długości 250-350ms w głównym wątku.

Pierwszą rzeczą, której próbowałem, było po prostu pozbywanie się tła MOC, aby wyeliminować możliwość, że może powodować problemy. W rzeczywistości pogorszyło to sytuację, ponieważ nasz kod aktualizacji lub tworzenia działał w głównym wątku i powodował obniżenie ogólnej wydajności animacji.

Zmiana stałego magazynu na NSInMemoryStoreType nie miało żadnego efektu.

Czy ktokolwiek może wskazać mi "tajny sos", który da mi wydajność UI, którą obiecał kontekst tła zarządzanego obiektu?


12
2017-10-28 15:28


pochodzenie


Wygląda na to, że obiekty, które zapisujesz, są następnie pomijane w głównym wątku. Czy mógłbyś uruchomić profiler danych podstawowych, aby zobaczyć, co powoduje błąd? Czy korzystasz z odwrotnych relacji? - ndfred
Wymelduję się z inspektora CD, ale używam odwrotnych relacji. - Ash Furrow


Odpowiedzi:


Mam kilka przypuszczeń, w kolejności, w jakiej powinieneś je sprawdzić:

  1. Wygląda na to, że wszystko, co się dzieje powoli, dzieje się po zapisaniu danych (tzn. W wywoływaniu zwrotnym). To prowadzi mnie do tego, że jest to albo przeładowanie PTSCollectionView lub ponowne pobranie pobranego kontrolera wyników.

  2. Czy ten problem występuje z UICollectionView w systemie iOS 6.x? Jeśli nie, to poprowadziłbym mnie do tego PTSCollectionView.

  3. Jeśli nadal się zdarza, oznacza to, że prawdopodobnie nie jest to widok kolekcji, ale jest to kontroler pobranych wyników. Wygląda na to, że z ramek stosu (jakkolwiek nieprzezroczyste) kontroler wyników próbuje wykonać pobieranie, które odbywa się przez dispatch_barrier. Są one stosowane w celu zapewnienia, że ​​blok nie zostanie wykonany, dopóki nie zostanie osiągnięta bariera. Jestem tutaj na kończynie, ale możesz chcieć to sprawdzić, ponieważ wewnętrznie, Core Data zapisuje gdzie indziej, a zatem opóźnia wykonanie jakichkolwiek innych żądań pobierania. Znowu to rodzaj dzikiego i niewykształconego przypuszczenia. Ale zrobiłbym to próbować bez pobieranego kontrolera wyników od razu i sprawdź czy twoje jąkanie nadal występuje.

Kolejną rzeczą, która mi się wyróżnia, jest to, że wykonujesz dużo pracy nad dzieckiem MOC ale następnie wykonując zapis na rodzica. Wygląda na to, że większość uratowania powinna wykonać dziecko. Ale może też być, że od jakiegoś czasu nie pracowałem z tą częścią Core Data :-)


0
2017-10-28 17:06





Podam kilka założeń, ale z twojego opisu myślę, że to rozsądne.

Po pierwsze zakładam, że główny MOC został utworzony za pomocą:

[[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

Zakładam to założenie, ponieważ ...

  1. Używasz go jako kontekstu nadrzędnego, więc musi to być albo NSMainQueueConcurrencyType lub NSPrivateQueueConcurrencyType.

  2. Ponieważ używasz go gdzie indziej, zrobiłbyś to tylko wtedy, gdyby zagwarantowano, że będziesz dostępny w głównej kolejce.

Teraz zakładam również, że główny MOC został podłączony bezpośrednio do stałego koordynatora sklepu, a nie do innego nadrzędnego MOC.

Kiedy MOC działający w tle wykonuje zapis, jego zmiany są propagowane do głównego MOC (choć nie są jeszcze zapisane). Ponieważ twój FRC jest dołączony do głównego MOC, natychmiast zobaczy te zmiany.

Kiedy dokonasz zapisu w głównym MOC, twój kod zostanie zablokowany, ponieważ ten zapis nie powróci, dopóki nie zakończy się zapis.

To, co widzisz, jest całkowicie oczekiwane.

Istnieje wiele opcji rozwiązania problemu.

Moją pierwszą propozycją byłoby utworzenie MOC-owej kolejki prywatnej i uczynienie go rodzicem głównej kolejki MOC.

Oznacza to, że każdy zapis głównej kolejki MOC będzie nie blok. Zamiast tego "zapisze" dane do jednostki nadrzędnej, która następnie wykona rzeczywiste zapisywanie bazy danych we własnej prywatnej kolejce, aw rezultacie w osobnym wątku w tle.

Teraz rozwiąże to problem głównego blokowania wątków. Ten mechanizm dobrze pasuje również do MOC-a, który ładuje bazę danych.

Zauważ, że istnieje kilka błędów związanych z kontekstami zagnieżdżonymi w systemie iOS 5, ale jeśli kierujesz na system iOS 6, większość z nich została naprawiona.

Widzieć Core Data nie może wypełnić błędu dla obiektu po uzyskaniuPermanantIDs po więcej informacji.

EDYTOWAĆ

Twoje założenia są poprawne, ale obawiam się, że celuję w system iOS 5   i może mieć tylko macierzysty MOC do głównego MOC na iOS 6 (powoduje to   impas z FRC). - Ash Bruzda

Jeśli widzisz zakleszczenie, najpierw pozbądź się swojego dispatch_sync i performBlockAndWait połączenia. Używanie operacji blokowania z poziomu głównego wątku nigdy nie powinno się odbywać dla niczego innego niż najbardziej prostą synchronizację (to jest zsynchronizowaną operację odczytu ze struktury danych) ... i tylko wtedy, gdy jest to konieczne. Również połączenia synchroniczne mogą powodować nieoczekiwany zakleszczenie, dlatego należy go unikać, gdy tylko jest to możliwe, szczególnie w przypadku wywoływania dowolnego kodu, który nie jest bezpośrednio kontrolowany.

Jeśli nie możesz tego zrobić, jest jeszcze kilka innych opcji. Najbardziej podoba mi się dołączenie FRC do prywatnej kolejki-MOC, a następnie "przyklejenie" twojego dostępu do / z FRC przez główny wątek. Możesz dispatch_async do głównego wątku, aby obsłużyć delegowane aktualizacje widoku tabeli (lub cokolwiek innego). Na przykład...

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    dispatch_async(dispatch_get_main_queue(), ^{
        [tableView beginUpdates];
    });
}

Możesz utworzyć proxy dla FRC, który jest tylko frontendem, aby upewnić się, że dostęp jest odpowiednio zsynchronizowany. Nie musi to być pełnoprocesorowość ... wystarczająca dla tego, co używasz FRC dla ... i zapewnienia, że ​​wszystkie operacje są odpowiednio zsynchronizowane.

To naprawdę łatwiejsze niż się wydaje.

W efekcie będziesz miał dokładnie taką samą konfigurację jak teraz, z wyjątkiem "main-MOC" będzie MOC-em kolejki prywatnej zamiast MOC-kolejki głównej. W związku z tym główny wątek nie będzie blokowany, gdy nastąpi dostęp do bazy danych.

Musisz jednak podjąć kilka środków ostrożności, aby upewnić się, że używasz FRC w odpowiednim kontekście.

Inną opcją jest użycie twojego głównego MOC i FRC tak, jak robisz to teraz, aby przetwarzać zmiany w bazie danych, ale wszystkie modyfikacje bazy danych przechodzą przez oddzielny MOC, dołączony bezpośrednio do stałego koordynatora sklepu. Dzięki temu zmiany zachodzą w osobnym wątku. Następnie używasz powiadomień MOC, aby zaktualizować kontekst i / lub pobrać dane ze sklepu.

Aktualizacje systemu iOS i OSX rozwiązują wiele problemów związanych z zagnieżdżonymi kontekstami dla danych podstawowych, ale są pewne rzeczy, o które trzeba się martwić przy obsłudze poprzedniej wersji.


6
2017-10-29 01:15



Twoje założenia są poprawne, ale obawiam się, że celuję w system iOS 5 i mogę mieć tylko nadrzędny MOC na głównym MOC na iOS 6 (powoduje on zakleszczenie z FRC). - Ash Furrow
Zakleszczenie jest spowodowane problemem FRC dla systemu iOS 5, nie z powodu czegokolwiek w moim kodzie. Daję innym opcjom strzał. - Ash Furrow