Implementazione dell'importazione dei dati di base rapida ed efficiente su iOS 5


101

Domanda : come faccio a far sì che il mio contesto figlio veda le modifiche persistenti nel contesto padre in modo che attivino il mio NSFetchedResultsController per aggiornare l'interfaccia utente?

Ecco la configurazione:

Hai un'app che scarica e aggiunge molti dati XML (circa 2 milioni di record, ciascuno delle dimensioni di un normale paragrafo di testo) Il file .sqlite diventa di circa 500 MB. L'aggiunta di questo contenuto a Core Data richiede tempo, ma si desidera che l'utente sia in grado di utilizzare l'app mentre i dati vengono caricati nell'archivio dati in modo incrementale. Deve essere invisibile e impercettibile all'utente che grandi quantità di dati vengano spostati, quindi niente blocchi, niente nervosismo: scorre come il burro. Tuttavia, l'app è più utile, più dati vengono aggiunti, quindi non possiamo aspettare per sempre che i dati vengano aggiunti al Core Data Store. Nel codice questo significa che mi piacerebbe davvero evitare codice come questo nel codice di importazione:

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

L'app è solo iOS 5, quindi il dispositivo più lento che deve supportare è un iPhone 3GS.

Ecco le risorse che ho utilizzato finora per sviluppare la mia soluzione attuale:

Guida alla programmazione dei dati principali di Apple: importazione efficiente dei dati

  • Usa i pool di rilascio automatico per mantenere bassa la memoria
  • Costo delle relazioni. Importa flat, quindi aggiusta le relazioni alla fine
  • Non interrogare se puoi evitarlo, rallenta le cose in modo O (n ^ 2)
  • Importa in batch: salva, ripristina, scarica e ripeti
  • Disattiva Gestione annullamenti durante l'importazione

iDeveloper TV: prestazioni dei dati fondamentali

  • Usa 3 contesti: tipi di contesto principale, principale e confinato

iDeveloper TV - Aggiornamento dati principali per Mac, iPhone e iPad

  • L'esecuzione di salvataggi su altre code con performBlock rende le cose veloci.
  • La crittografia rallenta le cose, disattivala se puoi.

Importazione e visualizzazione di set di dati di grandi dimensioni nei dati principali di Marcus Zarra

  • Puoi rallentare l'importazione dando tempo al ciclo di esecuzione corrente, in modo che le cose sembrino fluide per l'utente.
  • Il codice di esempio dimostra che è possibile eseguire grandi importazioni e mantenere reattiva l'interfaccia utente, ma non così velocemente come con 3 contesti e salvataggio asincrono su disco.

La mia soluzione attuale

Ho 3 istanze di NSManagedObjectContext:

masterManagedObjectContext - Questo è il contesto che ha NSPersistentStoreCoordinator ed è responsabile del salvataggio su disco. Lo faccio in modo che i miei salvataggi possano essere asincroni e quindi molto veloci. Lo creo al lancio in questo modo:

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

mainManagedObjectContext : questo è il contesto che l'interfaccia utente utilizza ovunque. È un figlio di masterManagedObjectContext. Lo creo così:

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

backgroundContext : questo contesto viene creato nella mia sottoclasse NSOperation che è responsabile dell'importazione dei dati XML in Core Data. Lo creo nel metodo principale dell'operazione e lo collego al contesto master lì.

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

Funziona in realtà molto, MOLTO veloce. Solo eseguendo questa configurazione in 3 contesti sono stato in grado di migliorare la mia velocità di importazione di oltre 10 volte! Onestamente, questo è difficile da credere. (Questo design di base dovrebbe far parte del modello Core Data standard ...)

Durante il processo di importazione salvo 2 modi diversi. Ogni 1000 elementi che salvo nel contesto di sfondo:

BOOL saveSuccess = [backgroundContext save:&error];

Quindi, alla fine del processo di importazione, salvo sul contesto principale / genitore che, apparentemente, spinge le modifiche agli altri contesti figlio incluso il contesto principale:

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

Problema : il problema è che la mia interfaccia utente non si aggiornerà finché non ricaricherò la visualizzazione.

Ho un semplice UIViewController con un UITableView che riceve dati utilizzando un NSFetchedResultsController. Al termine del processo di importazione, NSFetchedResultsController non vede modifiche dal contesto padre / master e quindi l'interfaccia utente non si aggiorna automaticamente come sono abituato a vedere. Se estraggo l'UIViewController dallo stack e lo carico di nuovo, tutti i dati sono lì.

Domanda : come faccio a far sì che il mio contesto figlio veda le modifiche persistenti nel contesto padre in modo che attivino il mio NSFetchedResultsController per aggiornare l'interfaccia utente?

Ho provato quanto segue che blocca l'app:

- (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 per la domanda più formata e preparata di sempre. Ho anche una risposta ... Ci vorranno alcuni minuti per
scriverla

1
Quando dici che l'app è bloccata, dov'è? Cosa sta facendo?
Jody Hagins,

Mi dispiace sollevare questo problema dopo molto tempo. Potete per favore chiarire cosa significa "Importa flat, quindi riattacca le relazioni alla fine"? Non devi ancora avere quegli oggetti in memoria per stabilire relazioni? Sto cercando di implementare una soluzione molto simile alla tua e potrei davvero usare qualche aiuto per abbassare l'impronta di memoria.
Andrea Sprega

Vedi gli Apple Docs collegati al primo di questo articolo. Spiega questo. In bocca al lupo!
David Weiss

1
Domanda davvero buona e ho raccolto alcuni trucchi dalla descrizione che hai fornito della tua configurazione
djskinner

Risposte:


47

Probabilmente dovresti salvare anche il master MOC a passi da gigante. Non ha senso che il MOC aspetti fino alla fine per salvare. Ha il suo thread e aiuterà anche a mantenere bassa la memoria.

Hai scritto:

Quindi, alla fine del processo di importazione, salvo sul contesto principale / genitore che, apparentemente, spinge le modifiche agli altri contesti figlio incluso il contesto principale:

Nella tua configurazione, hai due figli (il MOC principale e il MOC di sfondo), entrambi parentati dal "master".

Quando si salva su un bambino, le modifiche vengono trasferite al genitore. Gli altri figli di quel MOC vedranno i dati la prossima volta che eseguiranno un recupero ... non vengono informati esplicitamente.

Quindi, quando BG salva, i suoi dati vengono inviati a MASTER. Notare, tuttavia, che nessuno di questi dati è su disco finché MASTER non salva. Inoltre, qualsiasi nuovo elemento non riceverà ID permanenti fino a quando il MASTER non salverà su disco.

Nel tuo scenario, stai estraendo i dati nel MAIN MOC unendoli dal salvataggio MASTER durante la notifica DidSave.

Dovrebbe funzionare, quindi sono curioso di sapere dove è "appeso". Noterò che non stai eseguendo il thread MOC principale nel modo canonico (almeno non per iOS 5).

Inoltre, probabilmente sei interessato solo a unire le modifiche dal MOC principale (anche se la tua registrazione sembra che sia comunque solo per quello). Se dovessi utilizzare la notifica di aggiornamento al salvataggio, lo farei ...

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

Ora, per quello che potrebbe essere il tuo vero problema riguardo all'hang ... mostri due chiamate diverse per salvare sul master. il primo è ben protetto nel proprio performBlock, ma il secondo no (anche se potresti chiamare saveMasterContext in un performBlock ...

Tuttavia, cambierei anche questo codice ...

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

Tuttavia, si noti che il MAIN è un figlio di MASTER. Quindi, non dovrebbe essere necessario unire le modifiche. Invece, guarda il DidSave sul master e ripeti il ​​caricamento! I dati sono già presenti nei tuoi genitori, aspettano solo che tu li chieda. Questo è uno dei vantaggi di avere i dati nel genitore in primo luogo.

Un'altra alternativa da considerare (e sarei interessato a conoscere i tuoi risultati - sono molti dati) ...

Invece di rendere il MOC di sfondo un figlio del MASTER, rendilo un figlio del MAIN.

Prendi questo. Ogni volta che il BG salva, viene automaticamente inserito nella MAIN. Ora, il MAIN deve chiamare save, e poi il master deve chiamare save, ma tutto ciò che stanno facendo è spostare i puntatori ... finché il master non salva su disco.

La bellezza di questo metodo è che i dati passano dal MOC in background direttamente al MOC delle applicazioni (quindi passano per essere salvati).

C'è qualche penalità per il passaggio, ma tutto il sollevamento pesante viene eseguito nel MASTER quando colpisce il disco. E se si kickano quei salvataggi sul master con performBlock, il thread principale invia semplicemente la richiesta e ritorna immediatamente.

Per favore fammi sapere come va!


Ottima risposta. Proverò queste idee oggi e vedrò cosa scopro. Grazie!
David Weiss,

Eccezionale! Ha funzionato perfettamente! Tuttavia, proverò il tuo suggerimento di MASTER -> MAIN -> BG e vedrò come funziona quella performance, sembra un'idea molto interessante. Grazie per le grandi idee!
David Weiss

4
Aggiornato per modificare performBlockAndWait in performBlock. Non sono sicuro del motivo per cui questo è apparso di nuovo nella mia coda, ma quando l'ho letto questa volta, era ovvio ... non sono sicuro del motivo per cui l'ho lasciato andare prima. Sì, performBlockAndWait è rientrante. Tuttavia, in un ambiente nidificato come questo, non è possibile chiamare la versione sincrona su un contesto figlio dall'interno di un contesto padre. La notifica può essere (in questo caso viene) inviata dal contesto padre, il che può causare un deadlock. Spero che sia chiaro a chiunque venga e legga questo più tardi. Grazie, David.
Jody Hagins

1
@DavidWeiss Hai provato MASTER -> MAIN -> BG? Sono interessato a questo modello di design e spero di sapere se funziona bene per te. Grazie.
nonamelive

2
Il problema con MASTER -> MAIN -> pattern BG è quando si recupera dal contesto BG, verrà anche recuperato da MAIN e ciò bloccherà l'interfaccia utente e renderà l'app non reattiva
Rostyslav
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.