Implementación de la importación de datos básicos rápida y eficiente en iOS 5

101

Pregunta : ¿Cómo consigo que el contexto de mi hijo vea los cambios persistentes en el contexto principal para que activen mi NSFetchedResultsController para actualizar la interfaz de usuario?

Aquí está la configuración:

Tiene una aplicación que descarga y agrega una gran cantidad de datos XML (aproximadamente 2 millones de registros, cada uno aproximadamente del tamaño de un párrafo de texto normal). El archivo .sqlite tiene un tamaño de aproximadamente 500 MB. Agregar este contenido a Core Data lleva tiempo, pero desea que el usuario pueda usar la aplicación mientras los datos se cargan en el almacén de datos de forma incremental. Tiene que ser invisible e imperceptible para el usuario que se muevan grandes cantidades de datos, por lo que no se cuelga, no hay nerviosismo: se desplaza como mantequilla. Aún así, la aplicación es más útil, cuantos más datos se le agregan, por lo que no podemos esperar una eternidad para que los datos se agreguen al almacén de datos centrales. En el código, esto significa que realmente me gustaría evitar un código como este en el código de importación:

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

La aplicación es solo para iOS 5, por lo que el dispositivo más lento que debe admitir es un iPhone 3GS.

Estos son los recursos que he usado hasta ahora para desarrollar mi solución actual:

Guía de programación de datos básicos de Apple: Importación de datos de forma eficiente

  • Utilice grupos de liberación automática para mantener baja la memoria
  • Costo de relaciones. Importe planos, luego repare las relaciones al final
  • No preguntes si puedes evitarlo, ralentiza las cosas de una manera O (n ^ 2)
  • Importar en lotes: guardar, restablecer, vaciar y repetir
  • Desactive el Administrador de deshacer al importar

iDeveloper TV: rendimiento de datos básicos

  • Utilice 3 contextos: tipos de contexto maestro, principal y confinamiento

iDeveloper TV - Actualización de Core Data para Mac, iPhone y iPad

  • La ejecución guarda en otras colas con performBlock agiliza las cosas.
  • El cifrado ralentiza las cosas, apáguelo si puede.

Importación y visualización de grandes conjuntos de datos en datos básicos por Marcus Zarra

  • Puede ralentizar la importación dando tiempo al ciclo de ejecución actual, de modo que las cosas se sientan bien para el usuario.
  • El código de muestra demuestra que es posible realizar grandes importaciones y mantener la interfaz de usuario receptiva, pero no tan rápido como con 3 contextos y el guardado asincrónico en disco.

Mi solución actual

Tengo 3 instancias de NSManagedObjectContext:

masterManagedObjectContext : este es el contexto que tiene NSPersistentStoreCoordinator y es responsable de guardar en el disco. Hago esto para que mis guardados puedan ser asincrónicos y, por lo tanto, muy rápidos. Lo creo en el lanzamiento así:

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

mainManagedObjectContext : este es el contexto que usa la interfaz de usuario en todas partes. Es un elemento secundario del masterManagedObjectContext. Lo creo así:

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

backgroundContext : este contexto se crea en mi subclase NSOperation que es responsable de importar los datos XML en Core Data. Lo creo en el método principal de la operación y lo vinculo al contexto maestro allí.

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

En realidad, esto funciona muy, MUY rápido. ¡Con solo hacer esta configuración de 3 contextos, pude mejorar mi velocidad de importación en más de 10 veces! Honestamente, esto es difícil de creer. (Este diseño básico debe ser parte de la plantilla de datos básicos estándar ...)

Durante el proceso de importación, guardo 2 formas diferentes. Cada 1000 elementos que guardo en el contexto de fondo:

BOOL saveSuccess = [backgroundContext save:&error];

Luego, al final del proceso de importación, guardo en el contexto maestro / padre que, aparentemente, empuja las modificaciones a los otros contextos secundarios, incluido el contexto principal:

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

Problema : el problema es que mi interfaz de usuario no se actualizará hasta que vuelva a cargar la vista.

Tengo un UIViewController simple con un UITableView que se alimenta de datos mediante un NSFetchedResultsController. Cuando se completa el proceso de importación, NSFetchedResultsController no ve cambios del contexto principal / maestro y, por lo tanto, la interfaz de usuario no se actualiza automáticamente como estoy acostumbrado a ver. Si saco el UIViewController de la pila y lo vuelvo a cargar, todos los datos están allí.

Pregunta : ¿Cómo consigo que el contexto de mi hijo vea los cambios persistentes en el contexto principal para que activen mi NSFetchedResultsController para actualizar la interfaz de usuario?

He intentado lo siguiente que simplemente cuelga la aplicación:

- (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
fuente
26
+1000000 para la pregunta mejor formada y más preparada. Yo también tengo una respuesta ... Sin embargo, tomará unos minutos escribirla ...
Jody Hagins
1
Cuando dices que la aplicación está colgada, ¿dónde está? ¿Que esta haciendo?
Jody Hagins
Siento mencionar esto después de mucho tiempo. ¿Puede aclarar qué significa "Importar plano y luego arreglar las relaciones al final"? ¿No es necesario tener todavía esos objetos en la memoria para establecer relaciones? Estoy tratando de implementar una solución muy similar a la suya y realmente me vendría bien un poco de ayuda para reducir la huella de memoria.
Andrea Sprega
Vea los Documentos de Apple vinculados al primero de este artículo. Explica esto. ¡Buena suerte!
David Weiss
1
Muy buena pregunta y
aprendí

Respuestas:

47

Probablemente también debería guardar el MOC maestro a pasos agigantados. No tiene sentido que ese MOC espere hasta el final para ahorrar. Tiene su propio hilo y también ayudará a reducir la memoria.

Tu escribiste:

Luego, al final del proceso de importación, guardo en el contexto maestro / padre que, aparentemente, empuja las modificaciones a los otros contextos secundarios, incluido el contexto principal:

En su configuración, tiene dos hijos (el MOC principal y el MOC de fondo), ambos vinculados al "maestro".

Cuando ahorras en un hijo, los cambios se trasladan al padre. Otros hijos de ese MOC verán los datos la próxima vez que realicen una búsqueda ... no se les notifica explícitamente.

Entonces, cuando BG guarda, sus datos se envían a MASTER. Sin embargo, tenga en cuenta que ninguno de estos datos está en el disco hasta que MASTER los guarda. Además, los elementos nuevos no obtendrán ID permanentes hasta que MASTER los guarde en el disco.

En su escenario, está introduciendo los datos en el MOC PRINCIPAL fusionando desde el guardado MAESTRO durante la notificación DidSave.

Eso debería funcionar, así que tengo curiosidad por saber dónde está "colgado". Notaré que no se está ejecutando en el hilo principal de MOC de la forma canónica (al menos no para iOS 5).

Además, probablemente solo esté interesado en fusionar los cambios del MOC maestro (aunque su registro parece que es solo para eso de todos modos). Si tuviera que usar la notificación de actualización al guardar, haría esto ...

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

Ahora, para lo que puede ser su problema real con respecto al bloqueo ... muestra dos llamadas diferentes para guardar en el maestro. el primero está bien protegido en su propio performBlock, pero el segundo no (aunque es posible que esté llamando a saveMasterContext en un performBlock ...

Sin embargo, también cambiaría este código ...

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

Sin embargo, tenga en cuenta que MAIN es un hijo de MASTER. Por lo tanto, no debería tener que fusionar los cambios. En su lugar, solo busque DidSave en el maestro y vuelva a buscar. Los datos ya están en tu padre, esperando que los solicites. Ese es uno de los beneficios de tener los datos en el padre en primer lugar.

Otra alternativa a considerar (y me interesaría conocer sus resultados, son muchos datos) ...

En lugar de hacer que el MOC de fondo sea un hijo del MASTER, conviértalo en un hijo del MAIN.

Toma esto. Cada vez que el BG se guarda, se empuja automáticamente al PRINCIPAL. Ahora, el MAIN tiene que llamar a save, y luego el maestro tiene que llamar a save, pero lo único que hacen es mover punteros ... hasta que el maestro guarda en el disco.

La belleza de ese método es que los datos van desde el MOC de fondo directamente al MOC de sus aplicaciones (luego pasan a través para guardarse).

Hay alguna penalización por el traspaso, pero todo el trabajo pesado se hace en el MASTER cuando golpea el disco. Y si patea esos guardados en el maestro con performBlock, entonces el hilo principal simplemente envía la solicitud y regresa inmediatamente.

¡Por favor déjame saber cómo va!

Jody Hagins
fuente
Excelente respuesta. Probaré estas ideas hoy y veré qué descubro. ¡Gracias!
David Weiss
¡Increíble! ¡Eso funcionó perfectamente! Aún así, voy a probar tu sugerencia de MASTER -> MAIN -> BG y ver cómo funciona esa actuación, parece una idea muy interesante. ¡Gracias por las grandes ideas!
David Weiss
4
Actualizado para cambiar performBlockAndWait a performBlock. No estoy seguro de por qué esto volvió a aparecer en mi cola, pero cuando lo leí esta vez, era obvio ... no estoy seguro de por qué lo dejé pasar antes. Sí, performBlockAndWait es reentrante. Sin embargo, en un entorno anidado como este, no puede llamar a la versión sincrónica en un contexto secundario desde dentro de un contexto principal. La notificación puede enviarse (en este caso) desde el contexto principal, lo que puede provocar un interbloqueo. Espero que quede claro para cualquiera que venga y lea esto más tarde. Gracias, David.
Jody Hagins
1
@DavidWeiss ¿Has probado MASTER -> MAIN -> BG? Estoy interesado en este patrón de diseño y espero saber si le funciona bien. Gracias.
finaliza el
2
El problema con MASTER -> MAIN -> BG pattern es cuando se obtiene del contexto BG, también se obtendrá de MAIN y eso bloqueará la interfaz de usuario y hará que la aplicación no responda
Rostyslav