La aplicación se bloquea al guardar UIManagedDocument

Tengo una aplicación que primero carga algunos datos en un UIManagedDocument, luego ejecuta saveToURL:forSaveOperation:completionHandler:. Dentro del bloque completeHandler, realiza una actualización de varios elementos de esta base de datos, y cuando termina, realiza otro ahorro.

Además de eso, la aplicación tiene 3 botones que recargan los datos, reactualizan los datos y eliminan una entidad de la base de datos, respectivamente. En cada método de botón, la última instrucción también es un ahorro.

Cuando ejecuto todo esto en el simulador, todo va bien. Pero en el dispositivo no lo hace. Se bloquea constantemente. He observado que, normalmente, se bloquea al presionar el botón "eliminar", o al recargar o actualizar la base de datos. Y siempre está en el saveToURL operación.
En mi opinión, el problema viene cuando hay múltiples hilos guardando la base de datos. Como el dispositivo ejecuta el código más lentamente, es posible que se produzcan varios ahorros al mismo tiempo y la aplicación no pueda manejarlos correctamente. Además, a veces el botón Eliminar no elimina la entidad y dice que no existe (cuando existe).

Estoy totalmente desconcertado con esto, y todas estas operaciones de guardado deben hacerse... De hecho, si las elimino, la aplicación se comporta aún más incoherentemente.

¿Alguna sugerencia de qué podría hacer para resolver este problema? ¡Muchos gracias!

[Editar] Aquí publico el código problemático. Para cargar primero los datos, uso una clase auxiliar, con estos dos métodos en particular:

+ (void)loadDataIntoDatabase:(UIManagedDocument *)database
{
    [database.managedObjectContext performBlock:^{
        // Read from de plist file and fill the database
        [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
            [DataHelper completeDataOfDatabase:database];
        }];
}

+ (void)completeDataOfDatabase:(UIManagedDocument *)database
{
    [database.managedObjectContext performBlock:^{

        // Read from another plist file and update some parameters of the already existent data (uses NSFetchRequest and works well)

        // [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
        [database updateChangeCount:UIDocumentChangeDone];

    }];
}  

Y en la vista, tengo 3 métodos de acción, como estos:

- (IBAction)deleteButton {

    [self.database.managedObjectContext performBlock:^{
        NSManagedObject *results = ;// The item to delete
        [self.database.managedObjectContext deleteObject:results];

            //  [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
        [self.database updateChangeCount:UIDocumentChangeDone];
        }];
}

- (IBAction)reloadExtraDataButton {

    [DataHelper loadDataIntoDatabase:self.database];

    // [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
    [self.database updateChangeCount:UIDocumentChangeDone];

}

- (IBAction)refreshDataButton {

    [DataHelper completeDataOfDatabase:self.database];
    //[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
    [self.database updateChangeCount:UIDocumentChangeDone];
}

[Editar 2] Más código: en primer lugar, la vista inicial ejecuta viewDidLoad de esta manera:

- (void)viewDidLoad{
    [super viewDidLoad];
    self.database = [DataHelper openDatabaseAndUseBlock:^{
        [self setupFetchedResultsController];
    }];
}

Así es como se ve el método setupFetchedResultsController:

- (void)setupFetchedResultsController
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Some entity name"];
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)]];

    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                        managedObjectContext:self.database.managedObjectContext
                                                                          sectionNameKeyPath:nil
                                                                                   cacheName:nil];
}

Cada vista de la aplicación (tiene pestañas) tiene un setupFetchedResultsController diferente para mostrar las diferentes entidades que contiene la base de datos.

Ahora, en la clase auxiliar, este es el primer método de clase que se ejecuta, a través de viewDidLoad de cada vista:

+ (UIManagedDocument *)openDatabaseAndUseBlock:(completion_block_t)completionBlock
{
    NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    url = [url URLByAppendingPathComponent:@"Database"];
    UIManagedDocument *database = [[UIManagedDocument alloc] initWithFileURL:url];

    if (![[NSFileManager defaultManager] fileExistsAtPath:[database.fileURL path]]) {

        [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
            [self loadDataIntoDatabase:database];
            completionBlock();
        }];

    } else if (database.documentState == UIDocumentStateClosed) {
        // Existe, pero cerrado -> Abrir
        [database openWithCompletionHandler:^(BOOL success) {
            [self loadDataIntoDatabase:database];
            completionBlock();
        }];

    } else if (database.documentState == UIDocumentStateNormal) {
        [self loadDataIntoDatabase:database];
        completionBlock();
    }

    return database;
}

preguntado el 03 de mayo de 12 a las 11:05

1 Respuestas

Realmente no proporcionaste mucho código. La única pista real que diste fue que estás usando múltiples subprocesos.

UIManagedDocument tiene dos ManagedObjectContexts (uno especificado para la cola principal y el otro para una cola privada), pero solo se debe acceder a cada uno desde su propio subproceso.

Por lo tanto, solo debe usar ManagedDocument.managedObjectContext desde dentro del subproceso principal. Si desea usarlo desde otro subproceso, debe usar performBlock o performBlockAndWait. Del mismo modo, nunca puede saber que está ejecutando en el subproceso privado para el contexto principal, por lo que si desea hacer algo específicamente para el principal, debe usar performBlock*.

Finalmente, realmente no debería estar llamando a saveToURL, excepto cuando inicialmente crea la base de datos. UIManagedDocument se guardará automáticamente (en su propio tiempo).

Si desea alentarlo a que guarde antes, puede enviarle updateChangeCount: UIDocumentChangeDone para indicarle que tiene cambios que deben guardarse.

EDITAR

Solo debe llamar a saveToURL cuando cree el archivo por primera vez. Con UIManagedDocument, no hay necesidad de volver a llamarlo (y en realidad puede causar algunos problemas no deseados).

Básicamente, cuando crea el documento, NO configure su iVar hasta que se ejecute el controlador de finalización. De lo contrario, podría estar utilizando un documento en un estado parcial. En este caso, use un asistente, como este, en el controlador de finalización.

- (void)_document:(UIManagedDocument*)doc canBeUsed:(BOOL)canBeUsed
{
    dispatch_async(dispatch_get_main_queue(), ^{
        if (canBeUsed) {
            _document = doc;
            // Now, the document is ready.
            // Fire off a notification, or notify a delegate, and do whatever you
            // want... you really should not use the document until it's ready, but
            // as long as you leave it nil until it is ready any access will
            // just correctly do nothing.
        } else {
            _document = nil;
            // Do whatever you want if the document can not be used.
            // Unfortunately, there is no way to get the actual error unless
            // you subclass UIManagedDocument and override handleError
        }
    }];
}

Y para inicializar su documento, algo como...

- (id)initializeDocumentWithFileURL:(NSURL *)url
{
    if (!url) {
        url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
        url = [url URLByAppendingPathComponent:@"Default_Project_Database"];
    }
    UIManagedDocument *doc = [[UIManagedDocument alloc] initWithFileURL:url];

    if (![[NSFileManager defaultManager] fileExistsAtPath:[doc.fileURL path]]) {
        // The file does not exist, so we need to create it at the proper URL
        [doc saveToURL:doc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
            [self _document:doc canBeUsed:success];
        }];
    } else if (doc.documentState == UIDocumentStateClosed) {
        [doc openWithCompletionHandler:^(BOOL success) {
            [self _document:doc canBeUsed:success];
        }];
    } else {
        // You only need this if you allow a UIManagedDocument to be passed
        // in to this object -- in which case the code above that initializes
        // the <doc> variable will be conditional on what was passed...
        BOOL success = doc.documentState == UIDocumentStateNormal;
        [self _document:doc canBeUsed:success];
    }
}

El "patrón" anterior es necesario para asegurarse de que no utilice el documento hasta que esté completamente listo para su uso. Ahora, ese fragmento de código debería ser la única vez que llame a saveToURL.

Tenga en cuenta que, por definición, document.managedObjectContext es del tipo NSMainQueueConcurrencyType. Por lo tanto, si sabe que su código se ejecuta en el subproceso principal (como todas las devoluciones de llamada de la interfaz de usuario), no tiene que usar performBlock.

Sin embargo, si realmente está haciendo muchas cosas en segundo plano, considere...

- (void)backgroundLoadDataIntoDocument:(UIManagedDocument*)document
{
    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    moc.parentContext = document.managedObjectContext;
    [moc performBlock:^{
        // Do your loading in here, and shove everything into the local MOC.
        // If you are loading a lot of stuff from the 'net (or elsewhere),
        // consider doing it in strides, so you deliver objects to the document
        // a little at a time instead of all at the end.

        // When ready to save, call save on this MOC.  It will shove the data up
        // into the MOC of the document.
        NSrror *error = nil;
        if ([moc save:&error]) {
            // Probably don't have to synchronize calling updateChangeCount, but I do it anyway...
            [document.managedObjectContext performBlockAndWait:^{
                [document updateChangeCount:UIDocumentChangeDone];
            }];
        } else {
            // Handle error
        }
    }];
}

En lugar de vincular su MOC de fondo con el MOC principal, puede vincularlo con el parentContext. Cargar y luego guardar en él pondrá los cambios "encima" del MOC principal. El MOC principal verá esos cambios la próxima vez que realice una operación de búsqueda (tenga en cuenta las propiedades de NSFetchRequest).

NOTA: Algunas personas han informado (y también aparece como una nota en el libro de Erica Sadun), que después del primer saveToURL, debe cerrar y luego abrir para que todo funcione correctamente.

EDITAR

Esto se está haciendo muy largo. Si tuvieras más puntos, te sugiero una charla. En realidad, no podemos hacerlo a través de SO, pero podríamos hacerlo a través de otro medio. Intentaré ser breve, pero vuelva a leer lo que publiqué y preste especial atención porque su código sigue violando varios inquilinos.

Primero, en viewDidLoad(), está asignando directamente su documento al resultado de llamar a openDatabaseAndUseBlock. El documento no está en un estado utilizable En ese tiempo. No desea que se pueda acceder al documento hasta que se activen los controladores de finalización, lo que no sucederá antes de que openDatabaseAndUseBlock() regrese.

En segundo lugar, solo llame a saveToURL la primera vez que cree su base de datos (dentro de openDatabaseAndUseBlock()). No lo use en ningún otro lugar.

Tercero. Regístrese en el centro de notificaciones para recibir todos los eventos (solo regístrelos). Esto será de gran ayuda para la depuración, ya que puede ver lo que sucede.

Cuarto, subclase UIManagedDocument, y anule el handleError, y vea si se está llamando... es la única forma en que verá el NSError exacto si/cuando suceda.

3/4 son principalmente para ayudarlo a depurar, no es necesario para su código de producción.

Tengo una cita, así que tengo que parar ahora. Sin embargo, aborde esos problemas, y aquí está el

contestado el 05 de mayo de 12 a las 14:05

Gracias por la respuesta, Jody. Acabo de editar la publicación para incluir el código, para una mejor comprensión. Cambié todos los ahorros, excepto el primero con updateChangeCount pero no parece solucionar el bloqueo de la aplicación al presionar un botón (casi siempre el botón Eliminar). Además, al azar dice que no hay ningún elemento para eliminar cuando lo está (probablemente debido a que la base de datos aún no se ha guardado, estoy seguro de que la solicitud para encontrar el elemento es correcta). - David

Aún no lo tienes del todo bien. Desafortunadamente, se supone que debo invitar a algunos amigos a cenar a las 7:00, son las 6:43 y estoy a 45 minutos de casa. También tengo la sensación de que están sucediendo más cosas de las que insinúan esos fragmentos de código. Si puede publicar un poco más sobre lo que está haciendo, veré si puedo armar un ejemplo de UIManagedDocument que sea similar a lo que está haciendo esta noche. - jody hagins

¡Gracias de nuevo, Jody! He editado la publicación nuevamente para mostrar más código. ¡Espero que te ayude a tener una mejor idea! Encuentro mi código muy similar a lo que tienes publicado. Intenté incrustar el contenido de loadDataIntoDatabase: dentro dispatch_async(dispatch_get_main_queue(), ^{}); pero no parece cambiar nada. Tal vez estoy abordando todo esto de mala manera ... Solo quiero que la base de datos se llene de datos una vez, y luego actualice algunos parámetros al presionar un botón, leyéndolos de otro archivo que se modifica cada cierto tiempo. No debería ser tan complicado, creo... :S - David

Gracias por la ayuda, Jody. Estoy tratando de abordar esos problemas, pero sin éxito, desafortunadamente :( ¿Crees que podríamos hablar en algún tipo de chat o algo así? Te lo agradecería mucho, porque creo que estoy bastante perdido... - David

@JodyHagins Si uno solo debe llamar a saveToURL en la creación, ¿cuál es el punto de UIDocumentSaveForOverwrite? - Brad Thomas

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.